1 package org
.asamk
.signal
.manager
.storage
.senderKeys
;
3 import org
.asamk
.signal
.manager
.helper
.RecipientAddressResolver
;
4 import org
.asamk
.signal
.manager
.storage
.Database
;
5 import org
.asamk
.signal
.manager
.storage
.Utils
;
6 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
7 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientIdCreator
;
8 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientResolver
;
9 import org
.signal
.libsignal
.protocol
.SignalProtocolAddress
;
10 import org
.slf4j
.Logger
;
11 import org
.slf4j
.LoggerFactory
;
12 import org
.whispersystems
.signalservice
.api
.push
.DistributionId
;
13 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
15 import java
.sql
.Connection
;
16 import java
.sql
.ResultSet
;
17 import java
.sql
.SQLException
;
18 import java
.util
.Collection
;
21 import java
.util
.stream
.Collectors
;
23 public class SenderKeySharedStore
{
25 private final static Logger logger
= LoggerFactory
.getLogger(SenderKeySharedStore
.class);
26 private final static String TABLE_SENDER_KEY_SHARED
= "sender_key_shared";
28 private final Database database
;
29 private final RecipientIdCreator recipientIdCreator
;
30 private final RecipientResolver resolver
;
31 private final RecipientAddressResolver addressResolver
;
33 public static void createSql(Connection connection
) throws SQLException
{
34 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
35 try (final var statement
= connection
.createStatement()) {
36 statement
.executeUpdate("""
37 CREATE TABLE sender_key_shared (
38 _id INTEGER PRIMARY KEY,
39 recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
40 device_id INTEGER NOT NULL,
41 distribution_id BLOB NOT NULL,
42 timestamp INTEGER NOT NULL,
43 UNIQUE(recipient_id, device_id, distribution_id)
50 final Database database
,
51 final RecipientIdCreator recipientIdCreator
,
52 final RecipientAddressResolver addressResolver
,
53 final RecipientResolver resolver
55 this.database
= database
;
56 this.recipientIdCreator
= recipientIdCreator
;
57 this.addressResolver
= addressResolver
;
58 this.resolver
= resolver
;
61 public Set
<SignalProtocolAddress
> getSenderKeySharedWith(final DistributionId distributionId
) {
62 try (final var connection
= database
.getConnection()) {
65 SELECT s.recipient_id, s.device_id
67 WHERE s.distribution_id = ?
69 ).formatted(TABLE_SENDER_KEY_SHARED
);
70 try (final var statement
= connection
.prepareStatement(sql
)) {
71 statement
.setBytes(1, UuidUtil
.toByteArray(distributionId
.asUuid()));
72 return Utils
.executeQueryForStream(statement
, this::getSenderKeySharedEntryFromResultSet
)
73 .map(k
-> new SignalProtocolAddress(addressResolver
.resolveRecipientAddress(k
.recipientId())
74 .getIdentifier(), k
.deviceId()))
75 .collect(Collectors
.toSet());
77 } catch (SQLException e
) {
78 throw new RuntimeException("Failed read from shared sender key store", e
);
82 public void markSenderKeySharedWith(
83 final DistributionId distributionId
, final Collection
<SignalProtocolAddress
> addresses
85 final var newEntries
= addresses
.stream()
86 .map(a
-> new SenderKeySharedEntry(resolver
.resolveRecipient(a
.getName()), a
.getDeviceId()))
87 .collect(Collectors
.toSet());
89 try (final var connection
= database
.getConnection()) {
90 connection
.setAutoCommit(false);
91 markSenderKeysSharedWith(connection
, distributionId
, newEntries
);
93 } catch (SQLException e
) {
94 throw new RuntimeException("Failed update shared sender key store", e
);
98 public void clearSenderKeySharedWith(final Collection
<SignalProtocolAddress
> addresses
) {
99 final var entriesToDelete
= addresses
.stream()
100 .map(a
-> new SenderKeySharedEntry(resolver
.resolveRecipient(a
.getName()), a
.getDeviceId()))
101 .collect(Collectors
.toSet());
103 try (final var connection
= database
.getConnection()) {
104 connection
.setAutoCommit(false);
108 WHERE recipient_id = ? AND device_id = ?
110 ).formatted(TABLE_SENDER_KEY_SHARED
);
111 try (final var statement
= connection
.prepareStatement(sql
)) {
112 for (final var entry
: entriesToDelete
) {
113 statement
.setLong(1, entry
.recipientId().id());
114 statement
.setInt(2, entry
.deviceId());
115 statement
.executeUpdate();
119 } catch (SQLException e
) {
120 throw new RuntimeException("Failed update shared sender key store", e
);
124 public void deleteAll() {
125 try (final var connection
= database
.getConnection()) {
130 ).formatted(TABLE_SENDER_KEY_SHARED
);
131 try (final var statement
= connection
.prepareStatement(sql
)) {
132 statement
.executeUpdate();
134 } catch (SQLException e
) {
135 throw new RuntimeException("Failed update shared sender key store", e
);
139 public void deleteAllFor(final RecipientId recipientId
) {
140 try (final var connection
= database
.getConnection()) {
144 WHERE recipient_id = ?
146 ).formatted(TABLE_SENDER_KEY_SHARED
);
147 try (final var statement
= connection
.prepareStatement(sql
)) {
148 statement
.setLong(1, recipientId
.id());
149 statement
.executeUpdate();
151 } catch (SQLException e
) {
152 throw new RuntimeException("Failed update shared sender key store", e
);
156 public void deleteSharedWith(
157 final RecipientId recipientId
, final int deviceId
, final DistributionId distributionId
159 try (final var connection
= database
.getConnection()) {
163 WHERE recipient_id = ? AND device_id = ? AND distribution_id = ?
165 ).formatted(TABLE_SENDER_KEY_SHARED
);
166 try (final var statement
= connection
.prepareStatement(sql
)) {
167 statement
.setLong(1, recipientId
.id());
168 statement
.setInt(2, deviceId
);
169 statement
.setBytes(3, UuidUtil
.toByteArray(distributionId
.asUuid()));
170 statement
.executeUpdate();
172 } catch (SQLException e
) {
173 throw new RuntimeException("Failed update shared sender key store", e
);
177 public void deleteAllFor(final DistributionId distributionId
) {
178 try (final var connection
= database
.getConnection()) {
182 WHERE distribution_id = ?
184 ).formatted(TABLE_SENDER_KEY_SHARED
);
185 try (final var statement
= connection
.prepareStatement(sql
)) {
186 statement
.setBytes(1, UuidUtil
.toByteArray(distributionId
.asUuid()));
187 statement
.executeUpdate();
189 } catch (SQLException e
) {
190 throw new RuntimeException("Failed update shared sender key store", e
);
194 public void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
) {
195 try (final var connection
= database
.getConnection()) {
200 WHERE recipient_id = ?
202 ).formatted(TABLE_SENDER_KEY_SHARED
);
203 try (final var statement
= connection
.prepareStatement(sql
)) {
204 statement
.setLong(1, recipientId
.id());
205 statement
.setLong(2, toBeMergedRecipientId
.id());
206 statement
.executeUpdate();
208 } catch (SQLException e
) {
209 throw new RuntimeException("Failed update shared sender key store", e
);
213 void addLegacySenderKeysShared(final Map
<DistributionId
, Set
<SenderKeySharedEntry
>> sharedSenderKeys
) {
214 logger
.debug("Migrating legacy sender keys shared to database");
215 long start
= System
.nanoTime();
216 try (final var connection
= database
.getConnection()) {
217 connection
.setAutoCommit(false);
218 for (final var entry
: sharedSenderKeys
.entrySet()) {
219 markSenderKeysSharedWith(connection
, entry
.getKey(), entry
.getValue());
222 } catch (SQLException e
) {
223 throw new RuntimeException("Failed update shared sender key store", e
);
225 logger
.debug("Complete sender keys shared migration took {}ms", (System
.nanoTime() - start
) / 1000000);
228 private void markSenderKeysSharedWith(
229 final Connection connection
, final DistributionId distributionId
, final Set
<SenderKeySharedEntry
> newEntries
230 ) throws SQLException
{
233 INSERT OR REPLACE INTO %s (recipient_id, device_id, distribution_id, timestamp)
236 ).formatted(TABLE_SENDER_KEY_SHARED
);
237 try (final var statement
= connection
.prepareStatement(sql
)) {
238 for (final var entry
: newEntries
) {
239 statement
.setLong(1, entry
.recipientId().id());
240 statement
.setInt(2, entry
.deviceId());
241 statement
.setBytes(3, UuidUtil
.toByteArray(distributionId
.asUuid()));
242 statement
.setLong(4, System
.currentTimeMillis());
243 statement
.executeUpdate();
248 private SenderKeySharedEntry
getSenderKeySharedEntryFromResultSet(ResultSet resultSet
) throws SQLException
{
249 final var recipientId
= resultSet
.getLong("recipient_id");
250 final var deviceId
= resultSet
.getInt("device_id");
251 return new SenderKeySharedEntry(recipientIdCreator
.create(recipientId
), deviceId
);
254 record SenderKeySharedEntry(RecipientId recipientId
, int deviceId
) {}