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
.signal
.libsignal
.protocol
.InvalidMessageException
;
7 import org
.signal
.libsignal
.protocol
.SignalProtocolAddress
;
8 import org
.signal
.libsignal
.protocol
.groups
.state
.SenderKeyRecord
;
9 import org
.signal
.libsignal
.protocol
.groups
.state
.SenderKeyStore
;
10 import org
.slf4j
.Logger
;
11 import org
.slf4j
.LoggerFactory
;
12 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
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
;
19 import java
.util
.UUID
;
21 public class SenderKeyRecordStore
implements SenderKeyStore
{
23 private static final Logger logger
= LoggerFactory
.getLogger(SenderKeyRecordStore
.class);
24 private static final String TABLE_SENDER_KEY
= "sender_key";
26 private final Database database
;
28 public static void createSql(Connection connection
) throws SQLException
{
29 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
30 try (final var statement
= connection
.createStatement()) {
31 statement
.executeUpdate("""
32 CREATE TABLE sender_key (
33 _id INTEGER PRIMARY KEY,
34 address TEXT NOT NULL,
35 device_id INTEGER NOT NULL,
36 distribution_id BLOB NOT NULL,
38 created_timestamp INTEGER NOT NULL,
39 UNIQUE(address, device_id, distribution_id)
45 SenderKeyRecordStore(final Database database
) {
46 this.database
= database
;
50 public SenderKeyRecord
loadSenderKey(final SignalProtocolAddress address
, final UUID distributionId
) {
51 final var key
= getKey(address
, distributionId
);
53 try (final var connection
= database
.getConnection()) {
54 return loadSenderKey(connection
, key
);
55 } catch (SQLException e
) {
56 throw new RuntimeException("Failed read from sender key store", e
);
61 public void storeSenderKey(
62 final SignalProtocolAddress address
, final UUID distributionId
, final SenderKeyRecord
record
64 final var key
= getKey(address
, distributionId
);
66 try (final var connection
= database
.getConnection()) {
67 storeSenderKey(connection
, key
, record);
68 } catch (SQLException e
) {
69 throw new RuntimeException("Failed update sender key store", e
);
73 long getCreateTimeForKey(final ServiceId selfServiceId
, final int selfDeviceId
, final UUID distributionId
) {
76 SELECT s.created_timestamp
78 WHERE s.address = ? AND s.device_id = ? AND s.distribution_id = ?
80 ).formatted(TABLE_SENDER_KEY
);
81 try (final var connection
= database
.getConnection()) {
82 try (final var statement
= connection
.prepareStatement(sql
)) {
83 statement
.setString(1, selfServiceId
.toString());
84 statement
.setInt(2, selfDeviceId
);
85 statement
.setBytes(3, UuidUtil
.toByteArray(distributionId
));
86 return Utils
.executeQueryForOptional(statement
, res
-> res
.getLong("created_timestamp")).orElse(-1L);
88 } catch (SQLException e
) {
89 throw new RuntimeException("Failed read from sender key store", e
);
93 void deleteSenderKey(final ServiceId serviceId
, final UUID distributionId
) {
97 WHERE s.address = ? AND s.distribution_id = ?
99 ).formatted(TABLE_SENDER_KEY
);
100 try (final var connection
= database
.getConnection()) {
101 try (final var statement
= connection
.prepareStatement(sql
)) {
102 statement
.setString(1, serviceId
.toString());
103 statement
.setBytes(2, UuidUtil
.toByteArray(distributionId
));
104 statement
.executeUpdate();
106 } catch (SQLException e
) {
107 throw new RuntimeException("Failed update sender key store", e
);
114 """.formatted(TABLE_SENDER_KEY
);
115 try (final var connection
= database
.getConnection()) {
116 try (final var statement
= connection
.prepareStatement(sql
)) {
117 statement
.executeUpdate();
119 } catch (SQLException e
) {
120 throw new RuntimeException("Failed update sender key store", e
);
124 void deleteAllFor(final ServiceId serviceId
) {
125 try (final var connection
= database
.getConnection()) {
126 deleteAllFor(connection
, serviceId
);
127 } catch (SQLException e
) {
128 throw new RuntimeException("Failed update sender key store", e
);
132 void addLegacySenderKeys(final Collection
<Pair
<Key
, SenderKeyRecord
>> senderKeys
) {
133 logger
.debug("Migrating legacy sender keys to database");
134 long start
= System
.nanoTime();
135 try (final var connection
= database
.getConnection()) {
136 connection
.setAutoCommit(false);
137 for (final var pair
: senderKeys
) {
138 storeSenderKey(connection
, pair
.first(), pair
.second());
141 } catch (SQLException e
) {
142 throw new RuntimeException("Failed update sender keys store", e
);
144 logger
.debug("Complete sender keys migration took {}ms", (System
.nanoTime() - start
) / 1000000);
147 private Key
getKey(final SignalProtocolAddress address
, final UUID distributionId
) {
148 return new Key(address
.getName(), address
.getDeviceId(), distributionId
);
151 private SenderKeyRecord
loadSenderKey(final Connection connection
, final Key key
) throws SQLException
{
156 WHERE s.address = ? AND s.device_id = ? AND s.distribution_id = ?
158 ).formatted(TABLE_SENDER_KEY
);
159 try (final var statement
= connection
.prepareStatement(sql
)) {
160 statement
.setString(1, key
.address());
161 statement
.setInt(2, key
.deviceId());
162 statement
.setBytes(3, UuidUtil
.toByteArray(key
.distributionId()));
163 return Utils
.executeQueryForOptional(statement
, this::getSenderKeyRecordFromResultSet
).orElse(null);
167 private void storeSenderKey(
168 final Connection connection
, final Key key
, final SenderKeyRecord senderKeyRecord
169 ) throws SQLException
{
170 final var sqlUpdate
= """
173 WHERE address = ? AND device_id = ? and distribution_id = ?
174 """.formatted(TABLE_SENDER_KEY
);
175 try (final var statement
= connection
.prepareStatement(sqlUpdate
)) {
176 statement
.setBytes(1, senderKeyRecord
.serialize());
177 statement
.setString(2, key
.address());
178 statement
.setLong(3, key
.deviceId());
179 statement
.setBytes(4, UuidUtil
.toByteArray(key
.distributionId()));
180 final var rows
= statement
.executeUpdate();
186 // Record doesn't exist yet, creating a new one
187 final var sqlInsert
= (
189 INSERT OR REPLACE INTO %s (address, device_id, distribution_id, record, created_timestamp)
190 VALUES (?, ?, ?, ?, ?)
192 ).formatted(TABLE_SENDER_KEY
);
193 try (final var statement
= connection
.prepareStatement(sqlInsert
)) {
194 statement
.setString(1, key
.address());
195 statement
.setInt(2, key
.deviceId());
196 statement
.setBytes(3, UuidUtil
.toByteArray(key
.distributionId()));
197 statement
.setBytes(4, senderKeyRecord
.serialize());
198 statement
.setLong(5, System
.currentTimeMillis());
199 statement
.executeUpdate();
203 private void deleteAllFor(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
209 ).formatted(TABLE_SENDER_KEY
);
210 try (final var statement
= connection
.prepareStatement(sql
)) {
211 statement
.setString(1, serviceId
.toString());
212 statement
.executeUpdate();
216 private SenderKeyRecord
getSenderKeyRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
218 final var record = resultSet
.getBytes("record");
220 return new SenderKeyRecord(record);
221 } catch (InvalidMessageException e
) {
222 logger
.warn("Failed to load sender key, resetting: {}", e
.getMessage());
227 record Key(String address
, int deviceId
, UUID distributionId
) {}