import org.asamk.signal.manager.api.Contact;
import org.asamk.signal.manager.api.Pair;
+import org.asamk.signal.manager.api.PhoneNumberSharingMode;
import org.asamk.signal.manager.api.Profile;
import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.storage.Database;
aci TEXT UNIQUE,
pni TEXT UNIQUE,
unregistered_timestamp INTEGER,
+ discoverable INTEGER,
profile_key BLOB,
profile_key_credential BLOB,
needs_pni_signature INTEGER NOT NULL DEFAULT FALSE,
profile_avatar_url_path TEXT,
profile_mobile_coin_address BLOB,
profile_unidentified_access_mode TEXT,
- profile_capabilities TEXT
+ profile_capabilities TEXT,
+ profile_phone_number_sharing TEXT
) STRICT;
""");
}
r.number, r.aci, r.pni, r.username,
r.profile_key, r.profile_key_credential,
r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
- r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities,
+ r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
+ r.discoverable,
r.storage_record
FROM %s r
WHERE r._id = ?
r.number, r.aci, r.pni, r.username,
r.profile_key, r.profile_key_credential,
r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
- r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities,
+ r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
+ r.discoverable,
r.storage_record
FROM %s r
WHERE r.storage_id = ?
r.number, r.aci, r.pni, r.username,
r.profile_key, r.profile_key_credential,
r.given_name, r.family_name, r.nick_name, r.expiration_time, r.mute_until, r.hide_story, r.profile_sharing, r.color, r.blocked, r.archived, r.hidden, r.unregistered_timestamp,
- r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities,
+ r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing,
+ r.discoverable,
r.storage_record
FROM %s r
WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
}
}
- public void markUnregistered(final Set<String> unregisteredUsers) {
- logger.debug("Marking {} numbers as unregistered", unregisteredUsers.size());
+ public void markUndiscoverablePossiblyUnregistered(final Set<String> numbers) {
+ logger.debug("Marking {} numbers as unregistered", numbers.size());
try (final var connection = database.getConnection()) {
connection.setAutoCommit(false);
- for (final var number : unregisteredUsers) {
- final var recipient = findByNumber(connection, number);
- if (recipient.isPresent()) {
- final var recipientId = recipient.get().id();
- markUnregisteredAndSplitIfNecessary(connection, recipientId);
+ for (final var number : numbers) {
+ final var recipientAddress = findByNumber(connection, number);
+ if (recipientAddress.isPresent()) {
+ final var recipientId = recipientAddress.get().id();
+ markDiscoverable(connection, recipientId, false);
+ final var contact = getContact(connection, recipientId);
+ if (recipientAddress.get().address().aci().isEmpty() || contact.unregisteredTimestamp() != null) {
+ markUnregisteredAndSplitIfNecessary(connection, recipientId);
+ }
+ }
+ }
+ connection.commit();
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed update recipient store", e);
+ }
+ }
+
+ public void markDiscoverable(final Set<String> numbers) {
+ logger.debug("Marking {} numbers as discoverable", numbers.size());
+ try (final var connection = database.getConnection()) {
+ connection.setAutoCommit(false);
+ for (final var number : numbers) {
+ final var recipientAddress = findByNumber(connection, number);
+ if (recipientAddress.isPresent()) {
+ final var recipientId = recipientAddress.get().id();
+ markDiscoverable(connection, recipientId, true);
}
}
connection.commit();
}
}
+ public void markRegistered(final RecipientId recipientId, final boolean registered) {
+ logger.debug("Marking {} as registered={}", recipientId, registered);
+ try (final var connection = database.getConnection()) {
+ connection.setAutoCommit(false);
+ if (registered) {
+ markRegistered(connection, recipientId);
+ } else {
+ markUnregistered(connection, recipientId);
+ }
+ connection.commit();
+ } catch (SQLException e) {
+ throw new RuntimeException("Failed update recipient store", e);
+ }
+ }
+
private void markUnregisteredAndSplitIfNecessary(
final Connection connection, final RecipientId recipientId
) throws SQLException {
}
}
+ private void markDiscoverable(
+ final Connection connection, final RecipientId recipientId, final boolean discoverable
+ ) throws SQLException {
+ final var sql = (
+ """
+ UPDATE %s
+ SET discoverable = ?
+ WHERE _id = ?
+ """
+ ).formatted(TABLE_RECIPIENT);
+ try (final var statement = connection.prepareStatement(sql)) {
+ statement.setBoolean(1, discoverable);
+ statement.setLong(2, recipientId.id());
+ statement.executeUpdate();
+ }
+ }
+
private void markRegistered(
final Connection connection, final RecipientId recipientId
) throws SQLException {
final var sql = (
"""
UPDATE %s
- SET unregistered_timestamp = ?
- WHERE _id = ? AND unregistered_timestamp IS NULL
+ SET unregistered_timestamp = ?, discoverable = FALSE
+ WHERE _id = ?
"""
).formatted(TABLE_RECIPIENT);
try (final var statement = connection.prepareStatement(sql)) {
final var sql = (
"""
UPDATE %s
- SET profile_last_update_timestamp = ?, profile_given_name = ?, profile_family_name = ?, profile_about = ?, profile_about_emoji = ?, profile_avatar_url_path = ?, profile_mobile_coin_address = ?, profile_unidentified_access_mode = ?, profile_capabilities = ?
+ SET profile_last_update_timestamp = ?, profile_given_name = ?, profile_family_name = ?, profile_about = ?, profile_about_emoji = ?, profile_avatar_url_path = ?, profile_mobile_coin_address = ?, profile_unidentified_access_mode = ?, profile_capabilities = ?, profile_phone_number_sharing = ?
WHERE _id = ?
"""
).formatted(TABLE_RECIPIENT);
profile == null
? null
: profile.getCapabilities().stream().map(Enum::name).collect(Collectors.joining(",")));
- statement.setLong(10, recipientId.id());
+ statement.setString(10,
+ profile == null || profile.getPhoneNumberSharingMode() == null
+ ? null
+ : profile.getPhoneNumberSharingMode().name());
+ statement.setLong(11, recipientId.id());
statement.executeUpdate();
}
rotateStorageId(connection, recipientId);
public Profile getProfile(final Connection connection, final RecipientId recipientId) throws SQLException {
final var sql = (
"""
- SELECT r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities
+ SELECT r.profile_last_update_timestamp, r.profile_given_name, r.profile_family_name, r.profile_about, r.profile_about_emoji, r.profile_avatar_url_path, r.profile_mobile_coin_address, r.profile_unidentified_access_mode, r.profile_capabilities, r.profile_phone_number_sharing
FROM %s r
WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
"""
getProfileKeyFromResultSet(resultSet),
getExpiringProfileKeyCredentialFromResultSet(resultSet),
getProfileFromResultSet(resultSet),
+ getDiscoverableFromResultSet(resultSet),
getStorageRecordFromResultSet(resultSet));
}
unregisteredTimestamp == 0 ? null : unregisteredTimestamp);
}
+ private static Boolean getDiscoverableFromResultSet(final ResultSet resultSet) throws SQLException {
+ final var discoverable = resultSet.getBoolean("discoverable");
+ if (resultSet.wasNull()) {
+ return null;
+ }
+ return discoverable;
+ }
+
private Profile getProfileFromResultSet(ResultSet resultSet) throws SQLException {
final var profileCapabilities = resultSet.getString("profile_capabilities");
final var profileUnidentifiedAccessMode = resultSet.getString("profile_unidentified_access_mode");
: Arrays.stream(profileCapabilities.split(","))
.map(Profile.Capability::valueOfOrNull)
.filter(Objects::nonNull)
- .collect(Collectors.toSet()));
+ .collect(Collectors.toSet()),
+ PhoneNumberSharingMode.valueOfOrNull(resultSet.getString("profile_phone_number_sharing")));
}
private ProfileKey getProfileKeyFromResultSet(ResultSet resultSet) throws SQLException {
package org.asamk.signal.manager.util;
import org.asamk.signal.manager.api.Pair;
+import org.asamk.signal.manager.api.PhoneNumberSharingMode;
import org.asamk.signal.manager.api.Profile;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException;
import java.io.IOException;
import java.util.Base64;
import java.util.HashSet;
+import java.util.Optional;
public class ProfileUtils {
}
try {
- var name = decrypt(encryptedProfile.getName(), profileCipher);
- var about = trimZeros(decrypt(encryptedProfile.getAbout(), profileCipher));
- var aboutEmoji = trimZeros(decrypt(encryptedProfile.getAboutEmoji(), profileCipher));
+ var name = decryptString(encryptedProfile.getName(), profileCipher);
+ var about = decryptString(encryptedProfile.getAbout(), profileCipher);
+ var aboutEmoji = decryptString(encryptedProfile.getAboutEmoji(), profileCipher);
final var nameParts = splitName(name);
+ final var remotePhoneNumberSharing = decryptBoolean(encryptedProfile.getPhoneNumberSharing(),
+ profileCipher).map(v -> v ? PhoneNumberSharingMode.EVERYBODY : PhoneNumberSharingMode.NOBODY)
+ .orElse(null);
return new Profile(System.currentTimeMillis(),
nameParts.first(),
nameParts.second(),
profileCipher,
identityKey.getPublicKey()),
getUnidentifiedAccessMode(encryptedProfile, profileCipher),
- getCapabilities(encryptedProfile));
+ getCapabilities(encryptedProfile),
+ remotePhoneNumberSharing);
} catch (InvalidCiphertextException e) {
logger.debug("Failed to decrypt profile for {}", encryptedProfile.getServiceId(), e);
return null;
return capabilities;
}
- private static String decrypt(
- final String encryptedName, final ProfileCipher profileCipher
+ private static String decryptString(
+ final String encrypted, final ProfileCipher profileCipher
) throws InvalidCiphertextException {
try {
- return encryptedName == null
- ? null
- : new String(profileCipher.decrypt(Base64.getDecoder().decode(encryptedName)));
+ return encrypted == null ? null : profileCipher.decryptString(Base64.getDecoder().decode(encrypted));
} catch (IllegalArgumentException e) {
return null;
}
}
+ private static Optional<Boolean> decryptBoolean(
+ final String encrypted, final ProfileCipher profileCipher
+ ) throws InvalidCiphertextException {
+ try {
+ return encrypted == null
+ ? Optional.empty()
+ : profileCipher.decryptBoolean(Base64.getDecoder().decode(encrypted));
+ } catch (IllegalArgumentException e) {
+ return Optional.empty();
+ }
+ }
+
private static byte[] decryptAndVerifyMobileCoinAddress(
final byte[] encryptedPaymentAddress, final ProfileCipher profileCipher, final ECPublicKey publicKey
) throws InvalidCiphertextException {
default -> new Pair<>(parts[0], parts[1]);
};
}
-
- static String trimZeros(String str) {
- if (str == null) {
- return null;
- }
-
- int pos = str.indexOf(0);
- return pos == -1 ? str : str.substring(0, pos);
- }
}