From 854cb35d159bec1fbfdeccb5a891a63e644c3741 Mon Sep 17 00:00:00 2001 From: AsamK Date: Fri, 31 Jul 2026 13:17:33 +0200 Subject: [PATCH] Explicitly store pni signature verified --- .../helper/IncomingMessageHandler.java | 3 +- .../manager/storage/AccountDatabase.java | 10 +- .../recipients/LegacyRecipientStore2.java | 1 + .../manager/storage/recipients/Recipient.java | 29 ++++- .../storage/recipients/RecipientStore.java | 107 ++++++++++++++++-- .../syncStorage/ContactRecordProcessor.java | 1 + .../syncStorage/StorageSyncModels.java | 1 + 7 files changed, 138 insertions(+), 14 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 3bf92afd..8ded3a8f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -498,10 +498,11 @@ public final class IncomingMessageHandler { } logger.debug("Verified association of ACI {} with PNI {}", aci, pni); - account.getRecipientTrustedResolver() + final var recipientId = account.getRecipientTrustedResolver() .resolveRecipientTrusted(Optional.of(ACI.from(aci.getRawUuid())), Optional.of(pni), senderAddress.getNumber()); + account.getRecipientStore().markPniSignatureVerified(recipientId); return true; } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java b/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java index 43f00dd0..2d535f64 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/AccountDatabase.java @@ -33,7 +33,7 @@ import java.util.UUID; public class AccountDatabase extends Database { private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class); - private static final long DATABASE_VERSION = 29; + private static final long DATABASE_VERSION = 30; private AccountDatabase(final HikariDataSource dataSource) { super(logger, DATABASE_VERSION, dataSource); @@ -635,6 +635,14 @@ public class AccountDatabase extends Database { """); } } + if (oldVersion < 30) { + logger.debug("Updating database: Create pni_signature_verified column"); + try (final var statement = connection.createStatement()) { + statement.executeUpdate(""" + ALTER TABLE recipient ADD pni_signature_verified INTEGER NOT NULL DEFAULT FALSE; + """); + } + } } private static void createUuidMappingTable( diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore2.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore2.java index 9e8bf338..6bb83ab5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore2.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/LegacyRecipientStore2.java @@ -100,6 +100,7 @@ public class LegacyRecipientStore2 { profile, null, null, + false, null); }).collect(Collectors.toMap(Recipient::getRecipientId, r -> r)); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java index 52354129..d5fa9b68 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Recipient.java @@ -25,6 +25,8 @@ public class Recipient { private final Long unregisteredTimestamp; + private final boolean pniSignatureVerified; + private final byte[] storageRecord; public Recipient( @@ -36,6 +38,7 @@ public class Recipient { final Profile profile, final Boolean discoverable, final Long unregisteredTimestamp, + final boolean pniSignatureVerified, final byte[] storageRecord ) { this.recipientId = recipientId; @@ -46,6 +49,7 @@ public class Recipient { this.profile = profile; this.discoverable = discoverable; this.unregisteredTimestamp = unregisteredTimestamp; + this.pniSignatureVerified = pniSignatureVerified; this.storageRecord = storageRecord; } @@ -58,6 +62,7 @@ public class Recipient { profile = builder.profile; discoverable = builder.discoverable; unregisteredTimestamp = builder.unregisteredTimestamp; + pniSignatureVerified = builder.pniSignatureVerified; storageRecord = builder.storageRecord; } @@ -73,6 +78,9 @@ public class Recipient { builder.profileKey = copy.getProfileKey(); builder.expiringProfileKeyCredential = copy.getExpiringProfileKeyCredential(); builder.profile = copy.getProfile(); + builder.discoverable = copy.getDiscoverable(); + builder.unregisteredTimestamp = copy.getUnregisteredTimestamp(); + builder.pniSignatureVerified = copy.isPniSignatureVerified(); builder.storageRecord = copy.getStorageRecord(); return builder; } @@ -113,6 +121,10 @@ public class Recipient { return unregisteredTimestamp == null; } + public boolean isPniSignatureVerified() { + return pniSignatureVerified; + } + public byte[] getStorageRecord() { return storageRecord; } @@ -127,12 +139,19 @@ public class Recipient { && Objects.equals(contact, recipient.contact) && Objects.equals(profileKey, recipient.profileKey) && Objects.equals(expiringProfileKeyCredential, recipient.expiringProfileKeyCredential) - && Objects.equals(profile, recipient.profile); + && Objects.equals(profile, recipient.profile) + && pniSignatureVerified == recipient.pniSignatureVerified; } @Override public int hashCode() { - return Objects.hash(recipientId, address, contact, profileKey, expiringProfileKeyCredential, profile); + return Objects.hash(recipientId, + address, + contact, + profileKey, + expiringProfileKeyCredential, + profile, + pniSignatureVerified); } public static final class Builder { @@ -145,6 +164,7 @@ public class Recipient { private Profile profile; private Boolean discoverable; private Long unregisteredTimestamp; + private boolean pniSignatureVerified; private byte[] storageRecord; private Builder() { @@ -190,6 +210,11 @@ public class Recipient { return this; } + public Builder withPniSignatureVerified(final boolean val) { + pniSignatureVerified = val; + return this; + } + public Builder withStorageRecord(final byte[] val) { storageRecord = val; return this; diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index dbc7620c..1c246d3f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -54,14 +54,16 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re private static final int MAX_RECIPIENT_CACHE_SIZE = 2000; - private final Map recipientAddressCache = Collections.synchronizedMap( - new LinkedHashMap<>(16, 0.75f, true) { + private final Map recipientAddressCache = Collections.synchronizedMap(new LinkedHashMap<>( + 16, + 0.75f, + true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > MAX_RECIPIENT_CACHE_SIZE; - } - }); + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_RECIPIENT_CACHE_SIZE; + } + }); public static void createSql(Connection connection) throws SQLException { // When modifying the CREATE statement here, also add a migration in AccountDatabase.java @@ -80,6 +82,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re profile_key BLOB, profile_key_credential BLOB, needs_pni_signature INTEGER NOT NULL DEFAULT FALSE, + pni_signature_verified INTEGER NOT NULL DEFAULT FALSE, given_name TEXT, family_name TEXT, @@ -372,6 +375,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re SELECT r._id, r.number, r.aci, r.pni, r.username, r.profile_key, r.profile_key_credential, + r.pni_signature_verified, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, 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_phone_number_sharing, r.discoverable, @@ -392,6 +396,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re SELECT r._id, r.number, r.aci, r.pni, r.username, r.profile_key, r.profile_key_credential, + r.pni_signature_verified, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, 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_phone_number_sharing, r.discoverable, @@ -441,6 +446,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re SELECT r._id, r.number, r.aci, r.pni, r.username, r.profile_key, r.profile_key_credential, + r.pni_signature_verified, r.given_name, r.family_name, r.nick_name, r.nick_name_given_name, r.nick_name_family_name, r.note, r.expiration_time, r.expiration_time_version, 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_phone_number_sharing, r.discoverable, @@ -961,6 +967,69 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re } } + public void markPniSignatureVerified(final RecipientId recipientId) { + logger.debug("Marking {} as pni signature verified", recipientId); + try (final var connection = database.getConnection()) { + connection.setAutoCommit(false); + if (storePniSignatureVerified(connection, recipientId, true)) { + rotateStorageId(connection, recipientId); + } + connection.commit(); + } catch (SQLException e) { + throw new RuntimeException("Failed update recipient store", e); + } + } + + public boolean storePniSignatureVerified( + final Connection connection, + final RecipientId recipientId, + final boolean value + ) throws SQLException { + return storePniSignatureVerified(connection, recipientId, value, false); + } + + private boolean storePniSignatureVerified( + final Connection connection, + final RecipientId recipientId, + final boolean value, + final boolean force + ) throws SQLException { + if (!force && isPniSignatureVerified(connection, recipientId) == value) { + return false; + } + + final var sql = ( + """ + UPDATE %s + SET pni_signature_verified = ? + WHERE _id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setBoolean(1, value); + statement.setLong(2, recipientId.id()); + statement.executeUpdate(); + } + return true; + } + + private boolean isPniSignatureVerified( + final Connection connection, + final RecipientId recipientId + ) throws SQLException { + final var sql = ( + """ + SELECT pni_signature_verified + FROM %s + WHERE _id = ? + """ + ).formatted(TABLE_RECIPIENT); + try (final var statement = connection.prepareStatement(sql)) { + statement.setLong(1, recipientId.id()); + return Utils.executeQuerySingleRow(statement, resultSet -> resultSet.getBoolean("pni_signature_verified")); + } + } + public boolean needsPniSignature(final RecipientId recipientId) { try (final var connection = database.getConnection()) { final var sql = ( @@ -1255,6 +1324,9 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re final List toBeMergedRecipientIds ) throws SQLException { for (final var toBeMergedRecipientId : toBeMergedRecipientIds) { + if (isPniSignatureVerified(connection, toBeMergedRecipientId)) { + storePniSignatureVerified(connection, recipientId, true, true); + } recipientMergeHandler.mergeRecipients(connection, recipientId, toBeMergedRecipientId); deleteRecipient(connection, toBeMergedRecipientId); recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(toBeMergedRecipientId)); @@ -1349,7 +1421,12 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re final var sql = ( """ UPDATE %s - SET number = NULL, aci = NULL, pni = NULL, username = NULL, storage_id = NULL + SET number = NULL, + aci = NULL, + pni = NULL, + username = NULL, + storage_id = NULL, + pni_signature_verified = FALSE WHERE _id = ? """ ).formatted(TABLE_RECIPIENT); @@ -1365,10 +1442,18 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re final RecipientAddress address ) throws SQLException { recipientAddressCache.entrySet().removeIf(e -> e.getValue().id().equals(recipientId)); + final var existingAddress = resolveRecipientAddress(connection, recipientId); + final var keepPniSignatureVerified = Objects.equals(existingAddress.aci(), address.aci()) && Objects.equals( + existingAddress.pni(), + address.pni()); final var sql = ( """ UPDATE %s - SET number = ?, aci = ?, pni = ?, username = ? + SET number = ?, + aci = ?, + pni = ?, + username = ?, + pni_signature_verified = ? WHERE _id = ? """ ).formatted(TABLE_RECIPIENT); @@ -1377,7 +1462,8 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re statement.setString(2, address.aci().map(ACI::toString).orElse(null)); statement.setString(3, address.pni().map(PNI::toString).orElse(null)); statement.setString(4, address.username().orElse(null)); - statement.setLong(5, recipientId.id()); + statement.setBoolean(5, keepPniSignatureVerified && isPniSignatureVerified(connection, recipientId)); + statement.setLong(6, recipientId.id()); statement.executeUpdate(); } rotateStorageId(connection, recipientId); @@ -1595,6 +1681,7 @@ public class RecipientStore implements RecipientIdCreator, RecipientResolver, Re getProfileFromResultSet(resultSet), getDiscoverableFromResultSet(resultSet), getUnregisteredTimestampFromResultSet(resultSet), + resultSet.getBoolean("pni_signature_verified"), getStorageRecordFromResultSet(resultSet)); } diff --git a/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java b/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java index ed0b4546..a71f4adf 100644 --- a/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java +++ b/lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java @@ -357,6 +357,7 @@ public class ContactRecordProcessor extends DefaultStorageRecordProcessor