Store profile phone number sharing mode and discoverable state

This commit is contained in:
AsamK 2024-04-16 21:55:50 +02:00
parent 71de8e63cc
commit 7e0d4c9b89
12 changed files with 202 additions and 47 deletions

View File

@ -3,5 +3,16 @@ package org.asamk.signal.manager.api;
public enum PhoneNumberSharingMode { public enum PhoneNumberSharingMode {
EVERYBODY, EVERYBODY,
CONTACTS, CONTACTS,
NOBODY, NOBODY;
public static PhoneNumberSharingMode valueOfOrNull(String value) {
if (value == null) {
return null;
}
try {
return valueOf(value);
} catch (IllegalArgumentException ignored) {
return null;
}
}
} }

View File

@ -26,6 +26,8 @@ public class Profile {
private final Set<Capability> capabilities; private final Set<Capability> capabilities;
private final PhoneNumberSharingMode phoneNumberSharingMode;
public Profile( public Profile(
final long lastUpdateTimestamp, final long lastUpdateTimestamp,
final String givenName, final String givenName,
@ -35,7 +37,8 @@ public class Profile {
final String avatarUrlPath, final String avatarUrlPath,
final byte[] mobileCoinAddress, final byte[] mobileCoinAddress,
final UnidentifiedAccessMode unidentifiedAccessMode, final UnidentifiedAccessMode unidentifiedAccessMode,
final Set<Capability> capabilities final Set<Capability> capabilities,
final PhoneNumberSharingMode phoneNumberSharingMode
) { ) {
this.lastUpdateTimestamp = lastUpdateTimestamp; this.lastUpdateTimestamp = lastUpdateTimestamp;
this.givenName = givenName; this.givenName = givenName;
@ -46,6 +49,7 @@ public class Profile {
this.mobileCoinAddress = mobileCoinAddress; this.mobileCoinAddress = mobileCoinAddress;
this.unidentifiedAccessMode = unidentifiedAccessMode; this.unidentifiedAccessMode = unidentifiedAccessMode;
this.capabilities = capabilities; this.capabilities = capabilities;
this.phoneNumberSharingMode = phoneNumberSharingMode;
} }
private Profile(final Builder builder) { private Profile(final Builder builder) {
@ -58,6 +62,7 @@ public class Profile {
mobileCoinAddress = builder.mobileCoinAddress; mobileCoinAddress = builder.mobileCoinAddress;
unidentifiedAccessMode = builder.unidentifiedAccessMode; unidentifiedAccessMode = builder.unidentifiedAccessMode;
capabilities = builder.capabilities; capabilities = builder.capabilities;
phoneNumberSharingMode = builder.phoneNumberSharingMode;
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -136,6 +141,10 @@ public class Profile {
return capabilities; return capabilities;
} }
public PhoneNumberSharingMode getPhoneNumberSharingMode() {
return phoneNumberSharingMode;
}
public enum UnidentifiedAccessMode { public enum UnidentifiedAccessMode {
UNKNOWN, UNKNOWN,
DISABLED, DISABLED,
@ -200,6 +209,7 @@ public class Profile {
private byte[] mobileCoinAddress; private byte[] mobileCoinAddress;
private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
private Set<Capability> capabilities = Collections.emptySet(); private Set<Capability> capabilities = Collections.emptySet();
private PhoneNumberSharingMode phoneNumberSharingMode;
private long lastUpdateTimestamp = 0; private long lastUpdateTimestamp = 0;
private Builder() { private Builder() {
@ -240,6 +250,11 @@ public class Profile {
return this; return this;
} }
public Builder withPhoneNumberSharingMode(final PhoneNumberSharingMode val) {
phoneNumberSharingMode = val;
return this;
}
public Profile build() { public Profile build() {
return new Profile(this); return new Profile(this);
} }

View File

@ -20,13 +20,16 @@ public class Recipient {
private final Profile profile; private final Profile profile;
private final Boolean discoverable;
public Recipient( public Recipient(
final RecipientId recipientId, final RecipientId recipientId,
final RecipientAddress address, final RecipientAddress address,
final Contact contact, final Contact contact,
final ProfileKey profileKey, final ProfileKey profileKey,
final ExpiringProfileKeyCredential expiringProfileKeyCredential, final ExpiringProfileKeyCredential expiringProfileKeyCredential,
final Profile profile final Profile profile,
final Boolean discoverable
) { ) {
this.recipientId = recipientId; this.recipientId = recipientId;
this.address = address; this.address = address;
@ -34,6 +37,7 @@ public class Recipient {
this.profileKey = profileKey; this.profileKey = profileKey;
this.expiringProfileKeyCredential = expiringProfileKeyCredential; this.expiringProfileKeyCredential = expiringProfileKeyCredential;
this.profile = profile; this.profile = profile;
this.discoverable = discoverable;
} }
private Recipient(final Builder builder) { private Recipient(final Builder builder) {
@ -41,8 +45,9 @@ public class Recipient {
address = builder.address; address = builder.address;
contact = builder.contact; contact = builder.contact;
profileKey = builder.profileKey; profileKey = builder.profileKey;
expiringProfileKeyCredential = builder.expiringProfileKeyCredential1; expiringProfileKeyCredential = builder.expiringProfileKeyCredential;
profile = builder.profile; profile = builder.profile;
discoverable = builder.discoverable;
} }
public static Builder newBuilder() { public static Builder newBuilder() {
@ -55,7 +60,7 @@ public class Recipient {
builder.address = copy.getAddress(); builder.address = copy.getAddress();
builder.contact = copy.getContact(); builder.contact = copy.getContact();
builder.profileKey = copy.getProfileKey(); builder.profileKey = copy.getProfileKey();
builder.expiringProfileKeyCredential1 = copy.getExpiringProfileKeyCredential(); builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential();
builder.profile = copy.getProfile(); builder.profile = copy.getProfile();
return builder; return builder;
} }
@ -84,6 +89,10 @@ public class Recipient {
return profile; return profile;
} }
public Boolean getDiscoverable() {
return discoverable;
}
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {
if (this == o) return true; if (this == o) return true;
@ -108,8 +117,9 @@ public class Recipient {
private RecipientAddress address; private RecipientAddress address;
private Contact contact; private Contact contact;
private ProfileKey profileKey; private ProfileKey profileKey;
private ExpiringProfileKeyCredential expiringProfileKeyCredential1; private ExpiringProfileKeyCredential expiringProfileKeyCredential;
private Profile profile; private Profile profile;
private Boolean discoverable;
private Builder() { private Builder() {
} }
@ -135,7 +145,7 @@ public class Recipient {
} }
public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) { public Builder withExpiringProfileKeyCredential(final ExpiringProfileKeyCredential val) {
expiringProfileKeyCredential1 = val; expiringProfileKeyCredential = val;
return this; return this;
} }
@ -144,6 +154,11 @@ public class Recipient {
return this; return this;
} }
public Builder withDiscoverable(final Boolean val) {
discoverable = val;
return this;
}
public Recipient build() { public Recipient build() {
return new Recipient(this); return new Recipient(this);
} }

View File

@ -363,6 +363,7 @@ public final class ProfileHelper {
logger.trace("Storing profile"); logger.trace("Storing profile");
account.getProfileStore().storeProfile(recipientId, newProfile); account.getProfileStore().storeProfile(recipientId, newProfile);
account.getRecipientStore().markRegistered(recipientId, true);
logger.trace("Done handling retrieved profile"); logger.trace("Done handling retrieved profile");
}).doOnError(e -> { }).doOnError(e -> {
@ -374,6 +375,10 @@ public final class ProfileHelper {
.withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN) .withUnidentifiedAccessMode(Profile.UnidentifiedAccessMode.UNKNOWN)
.withCapabilities(Set.of()) .withCapabilities(Set.of())
.build(); .build();
if (e instanceof NotFoundException) {
logger.debug("Marking recipient {} as unregistered after 404 profile fetch.", recipientId);
account.getRecipientStore().markRegistered(recipientId, false);
}
account.getProfileStore().storeProfile(recipientId, newProfile); account.getProfileStore().storeProfile(recipientId, newProfile);
}); });

View File

@ -187,7 +187,8 @@ public class RecipientHelper {
final var unregisteredUsers = new HashSet<>(numbers); final var unregisteredUsers = new HashSet<>(numbers);
unregisteredUsers.removeAll(registeredUsers.keySet()); unregisteredUsers.removeAll(registeredUsers.keySet());
account.getRecipientStore().markUnregistered(unregisteredUsers); account.getRecipientStore().markUndiscoverablePossiblyUnregistered(unregisteredUsers);
account.getRecipientStore().markDiscoverable(registeredUsers.keySet());
return registeredUsers; return registeredUsers;
} }

View File

@ -1310,7 +1310,8 @@ public class ManagerImpl implements Manager {
s.getContact(), s.getContact(),
s.getProfileKey(), s.getProfileKey(),
s.getExpiringProfileKeyCredential(), s.getExpiringProfileKeyCredential(),
s.getProfile())) s.getProfile(),
s.getDiscoverable()))
.toList(); .toList();
} }

View File

@ -33,7 +33,7 @@ import java.util.UUID;
public class AccountDatabase extends Database { public class AccountDatabase extends Database {
private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class); private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
private static final long DATABASE_VERSION = 25; private static final long DATABASE_VERSION = 26;
private AccountDatabase(final HikariDataSource dataSource) { private AccountDatabase(final HikariDataSource dataSource) {
super(logger, DATABASE_VERSION, dataSource); super(logger, DATABASE_VERSION, dataSource);
@ -591,6 +591,15 @@ public class AccountDatabase extends Database {
"""); """);
} }
} }
if (oldVersion < 26) {
logger.debug("Updating database: Create discoverabel and profile_phone_number_sharing columns");
try (final var statement = connection.createStatement()) {
statement.executeUpdate("""
ALTER TABLE recipient ADD discoverable INTEGER;
ALTER TABLE recipient ADD profile_phone_number_sharing TEXT;
""");
}
}
} }
private static void createUuidMappingTable( private static void createUuidMappingTable(

View File

@ -914,7 +914,8 @@ public class SignalAccount implements Closeable {
: profile.getUnidentifiedAccess() != null : profile.getUnidentifiedAccess() != null
? Profile.UnidentifiedAccessMode.ENABLED ? Profile.UnidentifiedAccessMode.ENABLED
: Profile.UnidentifiedAccessMode.DISABLED, : Profile.UnidentifiedAccessMode.DISABLED,
capabilities); capabilities,
null);
getProfileStore().storeProfile(recipientId, newProfile); getProfileStore().storeProfile(recipientId, newProfile);
} }
} }

View File

@ -87,7 +87,8 @@ public class LegacyRecipientStore2 {
r.profile.capabilities.stream() r.profile.capabilities.stream()
.map(Profile.Capability::valueOfOrNull) .map(Profile.Capability::valueOfOrNull)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet())); .collect(Collectors.toSet()),
null);
} }
return new Recipient(recipientId, return new Recipient(recipientId,
@ -96,6 +97,7 @@ public class LegacyRecipientStore2 {
profileKey, profileKey,
expiringProfileKeyCredential, expiringProfileKeyCredential,
profile, profile,
null,
null); null);
}).collect(Collectors.toMap(Recipient::getRecipientId, r -> r)); }).collect(Collectors.toMap(Recipient::getRecipientId, r -> r));

View File

@ -21,6 +21,8 @@ public class Recipient {
private final Profile profile; private final Profile profile;
private final Boolean discoverable;
private final byte[] storageRecord; private final byte[] storageRecord;
public Recipient( public Recipient(
@ -30,6 +32,7 @@ public class Recipient {
final ProfileKey profileKey, final ProfileKey profileKey,
final ExpiringProfileKeyCredential expiringProfileKeyCredential, final ExpiringProfileKeyCredential expiringProfileKeyCredential,
final Profile profile, final Profile profile,
final Boolean discoverable,
final byte[] storageRecord final byte[] storageRecord
) { ) {
this.recipientId = recipientId; this.recipientId = recipientId;
@ -38,6 +41,7 @@ public class Recipient {
this.profileKey = profileKey; this.profileKey = profileKey;
this.expiringProfileKeyCredential = expiringProfileKeyCredential; this.expiringProfileKeyCredential = expiringProfileKeyCredential;
this.profile = profile; this.profile = profile;
this.discoverable = discoverable;
this.storageRecord = storageRecord; this.storageRecord = storageRecord;
} }
@ -48,6 +52,7 @@ public class Recipient {
profileKey = builder.profileKey; profileKey = builder.profileKey;
expiringProfileKeyCredential = builder.expiringProfileKeyCredential; expiringProfileKeyCredential = builder.expiringProfileKeyCredential;
profile = builder.profile; profile = builder.profile;
discoverable = builder.discoverable;
storageRecord = builder.storageRecord; storageRecord = builder.storageRecord;
} }
@ -91,6 +96,10 @@ public class Recipient {
return profile; return profile;
} }
public Boolean getDiscoverable() {
return discoverable;
}
public byte[] getStorageRecord() { public byte[] getStorageRecord() {
return storageRecord; return storageRecord;
} }
@ -121,6 +130,7 @@ public class Recipient {
private ProfileKey profileKey; private ProfileKey profileKey;
private ExpiringProfileKeyCredential expiringProfileKeyCredential; private ExpiringProfileKeyCredential expiringProfileKeyCredential;
private Profile profile; private Profile profile;
private Boolean discoverable;
private byte[] storageRecord; private byte[] storageRecord;
private Builder() { private Builder() {
@ -156,6 +166,11 @@ public class Recipient {
return this; return this;
} }
public Builder withDiscoverable(final Boolean val) {
discoverable = val;
return this;
}
public Builder withStorageRecord(final byte[] val) { public Builder withStorageRecord(final byte[] val) {
storageRecord = val; storageRecord = val;
return this; return this;

View File

@ -2,6 +2,7 @@ package org.asamk.signal.manager.storage.recipients;
import org.asamk.signal.manager.api.Contact; import org.asamk.signal.manager.api.Contact;
import org.asamk.signal.manager.api.Pair; 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.Profile;
import org.asamk.signal.manager.api.UnregisteredRecipientException; import org.asamk.signal.manager.api.UnregisteredRecipientException;
import org.asamk.signal.manager.storage.Database; import org.asamk.signal.manager.storage.Database;
@ -64,6 +65,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
aci TEXT UNIQUE, aci TEXT UNIQUE,
pni TEXT UNIQUE, pni TEXT UNIQUE,
unregistered_timestamp INTEGER, unregistered_timestamp INTEGER,
discoverable INTEGER,
profile_key BLOB, profile_key BLOB,
profile_key_credential BLOB, profile_key_credential BLOB,
needs_pni_signature INTEGER NOT NULL DEFAULT FALSE, needs_pni_signature INTEGER NOT NULL DEFAULT FALSE,
@ -92,7 +94,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
profile_avatar_url_path TEXT, profile_avatar_url_path TEXT,
profile_mobile_coin_address BLOB, profile_mobile_coin_address BLOB,
profile_unidentified_access_mode TEXT, profile_unidentified_access_mode TEXT,
profile_capabilities TEXT profile_capabilities TEXT,
profile_phone_number_sharing TEXT
) STRICT; ) STRICT;
"""); """);
} }
@ -354,7 +357,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
r.number, r.aci, r.pni, r.username, r.number, r.aci, r.pni, r.username,
r.profile_key, r.profile_key_credential, 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.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 r.storage_record
FROM %s r FROM %s r
WHERE r._id = ? WHERE r._id = ?
@ -373,7 +377,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
r.number, r.aci, r.pni, r.username, r.number, r.aci, r.pni, r.username,
r.profile_key, r.profile_key_credential, 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.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 r.storage_record
FROM %s r FROM %s r
WHERE r.storage_id = ? WHERE r.storage_id = ?
@ -409,7 +414,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
r.number, r.aci, r.pni, r.username, r.number, r.aci, r.pni, r.username,
r.profile_key, r.profile_key_credential, 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.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 r.storage_record
FROM %s r FROM %s r
WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s WHERE (r.number IS NOT NULL OR r.aci IS NOT NULL) AND %s
@ -898,15 +904,19 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
} }
} }
public void markUnregistered(final Set<String> unregisteredUsers) { public void markUndiscoverablePossiblyUnregistered(final Set<String> numbers) {
logger.debug("Marking {} numbers as unregistered", unregisteredUsers.size()); logger.debug("Marking {} numbers as unregistered", numbers.size());
try (final var connection = database.getConnection()) { try (final var connection = database.getConnection()) {
connection.setAutoCommit(false); connection.setAutoCommit(false);
for (final var number : unregisteredUsers) { for (final var number : numbers) {
final var recipient = findByNumber(connection, number); final var recipientAddress = findByNumber(connection, number);
if (recipient.isPresent()) { if (recipientAddress.isPresent()) {
final var recipientId = recipient.get().id(); final var recipientId = recipientAddress.get().id();
markUnregisteredAndSplitIfNecessary(connection, recipientId); markDiscoverable(connection, recipientId, false);
final var contact = getContact(connection, recipientId);
if (recipientAddress.get().address().aci().isEmpty() || contact.unregisteredTimestamp() != null) {
markUnregisteredAndSplitIfNecessary(connection, recipientId);
}
} }
} }
connection.commit(); connection.commit();
@ -915,6 +925,38 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
} }
} }
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();
} catch (SQLException e) {
throw new RuntimeException("Failed update recipient store", e);
}
}
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( private void markUnregisteredAndSplitIfNecessary(
final Connection connection, final RecipientId recipientId final Connection connection, final RecipientId recipientId
) throws SQLException { ) throws SQLException {
@ -927,6 +969,23 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
} }
} }
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( private void markRegistered(
final Connection connection, final RecipientId recipientId final Connection connection, final RecipientId recipientId
) throws SQLException { ) throws SQLException {
@ -949,8 +1008,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
final var sql = ( final var sql = (
""" """
UPDATE %s UPDATE %s
SET unregistered_timestamp = ? SET unregistered_timestamp = ?, discoverable = FALSE
WHERE _id = ? AND unregistered_timestamp IS NULL WHERE _id = ?
""" """
).formatted(TABLE_RECIPIENT); ).formatted(TABLE_RECIPIENT);
try (final var statement = connection.prepareStatement(sql)) { try (final var statement = connection.prepareStatement(sql)) {
@ -985,7 +1044,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
final var sql = ( final var sql = (
""" """
UPDATE %s 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 = ? WHERE _id = ?
""" """
).formatted(TABLE_RECIPIENT); ).formatted(TABLE_RECIPIENT);
@ -1002,7 +1061,11 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
profile == null profile == null
? null ? null
: profile.getCapabilities().stream().map(Enum::name).collect(Collectors.joining(","))); : 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(); statement.executeUpdate();
} }
rotateStorageId(connection, recipientId); rotateStorageId(connection, recipientId);
@ -1396,7 +1459,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
public Profile getProfile(final Connection connection, final RecipientId recipientId) throws SQLException { public Profile getProfile(final Connection connection, final RecipientId recipientId) throws SQLException {
final var sql = ( 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 FROM %s r
WHERE r._id = ? AND r.profile_capabilities IS NOT NULL WHERE r._id = ? AND r.profile_capabilities IS NOT NULL
""" """
@ -1431,6 +1494,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
getProfileKeyFromResultSet(resultSet), getProfileKeyFromResultSet(resultSet),
getExpiringProfileKeyCredentialFromResultSet(resultSet), getExpiringProfileKeyCredentialFromResultSet(resultSet),
getProfileFromResultSet(resultSet), getProfileFromResultSet(resultSet),
getDiscoverableFromResultSet(resultSet),
getStorageRecordFromResultSet(resultSet)); getStorageRecordFromResultSet(resultSet));
} }
@ -1453,6 +1517,14 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
unregisteredTimestamp == 0 ? null : unregisteredTimestamp); 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 { private Profile getProfileFromResultSet(ResultSet resultSet) throws SQLException {
final var profileCapabilities = resultSet.getString("profile_capabilities"); final var profileCapabilities = resultSet.getString("profile_capabilities");
final var profileUnidentifiedAccessMode = resultSet.getString("profile_unidentified_access_mode"); final var profileUnidentifiedAccessMode = resultSet.getString("profile_unidentified_access_mode");
@ -1471,7 +1543,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re
: Arrays.stream(profileCapabilities.split(",")) : Arrays.stream(profileCapabilities.split(","))
.map(Profile.Capability::valueOfOrNull) .map(Profile.Capability::valueOfOrNull)
.filter(Objects::nonNull) .filter(Objects::nonNull)
.collect(Collectors.toSet())); .collect(Collectors.toSet()),
PhoneNumberSharingMode.valueOfOrNull(resultSet.getString("profile_phone_number_sharing")));
} }
private ProfileKey getProfileKeyFromResultSet(ResultSet resultSet) throws SQLException { private ProfileKey getProfileKeyFromResultSet(ResultSet resultSet) throws SQLException {

View File

@ -1,6 +1,7 @@
package org.asamk.signal.manager.util; package org.asamk.signal.manager.util;
import org.asamk.signal.manager.api.Pair; 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.Profile;
import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.protocol.InvalidKeyException; import org.signal.libsignal.protocol.InvalidKeyException;
@ -16,6 +17,7 @@ import org.whispersystems.signalservice.internal.push.PaymentAddress;
import java.io.IOException; import java.io.IOException;
import java.util.Base64; import java.util.Base64;
import java.util.HashSet; import java.util.HashSet;
import java.util.Optional;
public class ProfileUtils { public class ProfileUtils {
@ -33,11 +35,14 @@ public class ProfileUtils {
} }
try { try {
var name = decrypt(encryptedProfile.getName(), profileCipher); var name = decryptString(encryptedProfile.getName(), profileCipher);
var about = trimZeros(decrypt(encryptedProfile.getAbout(), profileCipher)); var about = decryptString(encryptedProfile.getAbout(), profileCipher);
var aboutEmoji = trimZeros(decrypt(encryptedProfile.getAboutEmoji(), profileCipher)); var aboutEmoji = decryptString(encryptedProfile.getAboutEmoji(), profileCipher);
final var nameParts = splitName(name); 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(), return new Profile(System.currentTimeMillis(),
nameParts.first(), nameParts.first(),
nameParts.second(), nameParts.second(),
@ -50,7 +55,8 @@ public class ProfileUtils {
profileCipher, profileCipher,
identityKey.getPublicKey()), identityKey.getPublicKey()),
getUnidentifiedAccessMode(encryptedProfile, profileCipher), getUnidentifiedAccessMode(encryptedProfile, profileCipher),
getCapabilities(encryptedProfile)); getCapabilities(encryptedProfile),
remotePhoneNumberSharing);
} catch (InvalidCiphertextException e) { } catch (InvalidCiphertextException e) {
logger.debug("Failed to decrypt profile for {}", encryptedProfile.getServiceId(), e); logger.debug("Failed to decrypt profile for {}", encryptedProfile.getServiceId(), e);
return null; return null;
@ -83,18 +89,28 @@ public class ProfileUtils {
return capabilities; return capabilities;
} }
private static String decrypt( private static String decryptString(
final String encryptedName, final ProfileCipher profileCipher final String encrypted, final ProfileCipher profileCipher
) throws InvalidCiphertextException { ) throws InvalidCiphertextException {
try { try {
return encryptedName == null return encrypted == null ? null : profileCipher.decryptString(Base64.getDecoder().decode(encrypted));
? null
: new String(profileCipher.decrypt(Base64.getDecoder().decode(encryptedName)));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return null; 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( private static byte[] decryptAndVerifyMobileCoinAddress(
final byte[] encryptedPaymentAddress, final ProfileCipher profileCipher, final ECPublicKey publicKey final byte[] encryptedPaymentAddress, final ProfileCipher profileCipher, final ECPublicKey publicKey
) throws InvalidCiphertextException { ) throws InvalidCiphertextException {
@ -129,13 +145,4 @@ public class ProfileUtils {
default -> new Pair<>(parts[0], parts[1]); 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);
}
} }