1 package org
.asamk
.signal
.manager
.storage
.senderKeys
;
3 import org
.asamk
.signal
.manager
.api
.Pair
;
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
.RecipientResolver
;
8 import org
.signal
.libsignal
.protocol
.InvalidMessageException
;
9 import org
.signal
.libsignal
.protocol
.SignalProtocolAddress
;
10 import org
.signal
.libsignal
.protocol
.groups
.state
.SenderKeyRecord
;
11 import org
.signal
.libsignal
.protocol
.groups
.state
.SenderKeyStore
;
12 import org
.slf4j
.Logger
;
13 import org
.slf4j
.LoggerFactory
;
14 import org
.whispersystems
.signalservice
.api
.util
.UuidUtil
;
16 import java
.sql
.Connection
;
17 import java
.sql
.ResultSet
;
18 import java
.sql
.SQLException
;
19 import java
.util
.Collection
;
20 import java
.util
.UUID
;
22 public class SenderKeyRecordStore
implements SenderKeyStore
{
24 private final static Logger logger
= LoggerFactory
.getLogger(SenderKeyRecordStore
.class);
25 private final static String TABLE_SENDER_KEY
= "sender_key";
27 private final Database database
;
28 private final RecipientResolver resolver
;
30 public static void createSql(Connection connection
) throws SQLException
{
31 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
32 try (final var statement
= connection
.createStatement()) {
33 statement
.executeUpdate("""
34 CREATE TABLE sender_key (
35 _id INTEGER PRIMARY KEY,
36 recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
37 device_id INTEGER NOT NULL,
38 distribution_id BLOB NOT NULL,
40 created_timestamp INTEGER NOT NULL,
41 UNIQUE(recipient_id, device_id, distribution_id)
48 final Database database
, final RecipientResolver resolver
50 this.database
= database
;
51 this.resolver
= resolver
;
55 public SenderKeyRecord
loadSenderKey(final SignalProtocolAddress address
, final UUID distributionId
) {
56 final var key
= getKey(address
, distributionId
);
58 try (final var connection
= database
.getConnection()) {
59 return loadSenderKey(connection
, key
);
60 } catch (SQLException e
) {
61 throw new RuntimeException("Failed read from sender key store", e
);
66 public void storeSenderKey(
67 final SignalProtocolAddress address
, final UUID distributionId
, final SenderKeyRecord
record
69 final var key
= getKey(address
, distributionId
);
71 try (final var connection
= database
.getConnection()) {
72 storeSenderKey(connection
, key
, record);
73 } catch (SQLException e
) {
74 throw new RuntimeException("Failed update sender key store", e
);
78 long getCreateTimeForKey(final RecipientId selfRecipientId
, final int selfDeviceId
, final UUID distributionId
) {
81 SELECT s.created_timestamp
83 WHERE s.recipient_id = ? AND s.device_id = ? AND s.distribution_id = ?
85 ).formatted(TABLE_SENDER_KEY
);
86 try (final var connection
= database
.getConnection()) {
87 try (final var statement
= connection
.prepareStatement(sql
)) {
88 statement
.setLong(1, selfRecipientId
.id());
89 statement
.setInt(2, selfDeviceId
);
90 statement
.setBytes(3, UuidUtil
.toByteArray(distributionId
));
91 return Utils
.executeQueryForOptional(statement
, res
-> res
.getLong("created_timestamp")).orElse(-1L);
93 } catch (SQLException e
) {
94 throw new RuntimeException("Failed read from sender key store", e
);
98 void deleteSenderKey(final RecipientId recipientId
, final UUID distributionId
) {
102 WHERE s.recipient_id = ? AND s.distribution_id = ?
104 ).formatted(TABLE_SENDER_KEY
);
105 try (final var connection
= database
.getConnection()) {
106 try (final var statement
= connection
.prepareStatement(sql
)) {
107 statement
.setLong(1, recipientId
.id());
108 statement
.setBytes(2, UuidUtil
.toByteArray(distributionId
));
109 statement
.executeUpdate();
111 } catch (SQLException e
) {
112 throw new RuntimeException("Failed update sender key store", e
);
119 """.formatted(TABLE_SENDER_KEY
);
120 try (final var connection
= database
.getConnection()) {
121 try (final var statement
= connection
.prepareStatement(sql
)) {
122 statement
.executeUpdate();
124 } catch (SQLException e
) {
125 throw new RuntimeException("Failed update sender key store", e
);
129 void deleteAllFor(final RecipientId recipientId
) {
130 try (final var connection
= database
.getConnection()) {
131 deleteAllFor(connection
, recipientId
);
132 } catch (SQLException e
) {
133 throw new RuntimeException("Failed update sender key store", e
);
137 void mergeRecipients(RecipientId recipientId
, RecipientId toBeMergedRecipientId
) {
138 try (final var connection
= database
.getConnection()) {
139 connection
.setAutoCommit(false);
143 WHERE recipient_id = ?
144 """.formatted(TABLE_SENDER_KEY
);
145 try (final var statement
= connection
.prepareStatement(sql
)) {
146 statement
.setLong(1, recipientId
.id());
147 statement
.setLong(2, toBeMergedRecipientId
.id());
148 final var rows
= statement
.executeUpdate();
150 logger
.debug("Reassigned {} sender keys of to be merged recipient.", rows
);
153 // Delete all conflicting sender keys now
154 deleteAllFor(connection
, toBeMergedRecipientId
);
156 } catch (SQLException e
) {
157 throw new RuntimeException("Failed update sender key store", e
);
161 void addLegacySenderKeys(final Collection
<Pair
<Key
, SenderKeyRecord
>> senderKeys
) {
162 logger
.debug("Migrating legacy sender keys to database");
163 long start
= System
.nanoTime();
164 try (final var connection
= database
.getConnection()) {
165 connection
.setAutoCommit(false);
166 for (final var pair
: senderKeys
) {
167 storeSenderKey(connection
, pair
.first(), pair
.second());
170 } catch (SQLException e
) {
171 throw new RuntimeException("Failed update sender keys store", e
);
173 logger
.debug("Complete sender keys migration took {}ms", (System
.nanoTime() - start
) / 1000000);
177 * @param identifier can be either a serialized uuid or an e164 phone number
179 private RecipientId
resolveRecipient(String identifier
) {
180 return resolver
.resolveRecipient(identifier
);
183 private Key
getKey(final SignalProtocolAddress address
, final UUID distributionId
) {
184 final var recipientId
= resolveRecipient(address
.getName());
185 return new Key(recipientId
, address
.getDeviceId(), distributionId
);
188 private SenderKeyRecord
loadSenderKey(final Connection connection
, final Key key
) throws SQLException
{
193 WHERE s.recipient_id = ? AND s.device_id = ? AND s.distribution_id = ?
195 ).formatted(TABLE_SENDER_KEY
);
196 try (final var statement
= connection
.prepareStatement(sql
)) {
197 statement
.setLong(1, key
.recipientId().id());
198 statement
.setInt(2, key
.deviceId());
199 statement
.setBytes(3, UuidUtil
.toByteArray(key
.distributionId()));
200 return Utils
.executeQueryForOptional(statement
, this::getSenderKeyRecordFromResultSet
).orElse(null);
204 private void storeSenderKey(
205 final Connection connection
, final Key key
, final SenderKeyRecord senderKeyRecord
206 ) throws SQLException
{
207 final var sqlUpdate
= """
210 WHERE recipient_id = ? AND device_id = ? and distribution_id = ?
211 """.formatted(TABLE_SENDER_KEY
);
212 try (final var statement
= connection
.prepareStatement(sqlUpdate
)) {
213 statement
.setBytes(1, senderKeyRecord
.serialize());
214 statement
.setLong(2, key
.recipientId().id());
215 statement
.setLong(3, key
.deviceId());
216 statement
.setBytes(4, UuidUtil
.toByteArray(key
.distributionId()));
217 final var rows
= statement
.executeUpdate();
223 // Record doesn't exist yet, creating a new one
224 final var sqlInsert
= (
226 INSERT OR REPLACE INTO %s (recipient_id, device_id, distribution_id, record, created_timestamp)
227 VALUES (?, ?, ?, ?, ?)
229 ).formatted(TABLE_SENDER_KEY
);
230 try (final var statement
= connection
.prepareStatement(sqlInsert
)) {
231 statement
.setLong(1, key
.recipientId().id());
232 statement
.setInt(2, key
.deviceId());
233 statement
.setBytes(3, UuidUtil
.toByteArray(key
.distributionId()));
234 statement
.setBytes(4, senderKeyRecord
.serialize());
235 statement
.setLong(5, System
.currentTimeMillis());
236 statement
.executeUpdate();
240 private void deleteAllFor(final Connection connection
, final RecipientId recipientId
) throws SQLException
{
244 WHERE s.recipient_id = ?
246 ).formatted(TABLE_SENDER_KEY
);
247 try (final var statement
= connection
.prepareStatement(sql
)) {
248 statement
.setLong(1, recipientId
.id());
249 statement
.executeUpdate();
253 private SenderKeyRecord
getSenderKeyRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
255 final var record = resultSet
.getBytes("record");
257 return new SenderKeyRecord(record);
258 } catch (InvalidMessageException e
) {
259 logger
.warn("Failed to load sender key, resetting: {}", e
.getMessage());
264 record Key(RecipientId recipientId
, int deviceId
, UUID distributionId
) {}