1 package org
.asamk
.signal
.manager
.storage
.prekeys
;
3 import org
.asamk
.signal
.manager
.storage
.Database
;
4 import org
.asamk
.signal
.manager
.storage
.Utils
;
5 import org
.signal
.libsignal
.protocol
.InvalidKeyException
;
6 import org
.signal
.libsignal
.protocol
.InvalidKeyIdException
;
7 import org
.signal
.libsignal
.protocol
.ecc
.Curve
;
8 import org
.signal
.libsignal
.protocol
.ecc
.ECKeyPair
;
9 import org
.signal
.libsignal
.protocol
.state
.PreKeyRecord
;
10 import org
.slf4j
.Logger
;
11 import org
.slf4j
.LoggerFactory
;
12 import org
.whispersystems
.signalservice
.api
.SignalServicePreKeyStore
;
13 import org
.whispersystems
.signalservice
.api
.push
.ServiceIdType
;
15 import java
.sql
.Connection
;
16 import java
.sql
.ResultSet
;
17 import java
.sql
.SQLException
;
18 import java
.util
.Collection
;
20 public class PreKeyStore
implements SignalServicePreKeyStore
{
22 private static final String TABLE_PRE_KEY
= "pre_key";
23 private static final Logger logger
= LoggerFactory
.getLogger(PreKeyStore
.class);
25 private final Database database
;
26 private final int accountIdType
;
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 pre_key (
33 _id INTEGER PRIMARY KEY,
34 account_id_type INTEGER NOT NULL,
35 key_id INTEGER NOT NULL,
36 public_key BLOB NOT NULL,
37 private_key BLOB NOT NULL,
38 stale_timestamp INTEGER,
39 UNIQUE(account_id_type, key_id)
45 public PreKeyStore(final Database database
, final ServiceIdType serviceIdType
) {
46 this.database
= database
;
47 this.accountIdType
= Utils
.getAccountIdType(serviceIdType
);
51 public PreKeyRecord
loadPreKey(int preKeyId
) throws InvalidKeyIdException
{
52 final var preKey
= getPreKey(preKeyId
);
54 throw new InvalidKeyIdException("No such pre key record: " + preKeyId
);
60 public void storePreKey(int preKeyId
, PreKeyRecord
record) {
63 INSERT INTO %s (account_id_type, key_id, public_key, private_key)
66 ).formatted(TABLE_PRE_KEY
);
67 try (final var connection
= database
.getConnection()) {
68 try (final var statement
= connection
.prepareStatement(sql
)) {
69 statement
.setInt(1, accountIdType
);
70 statement
.setInt(2, preKeyId
);
71 final var keyPair
= record.getKeyPair();
72 statement
.setBytes(3, keyPair
.getPublicKey().serialize());
73 statement
.setBytes(4, keyPair
.getPrivateKey().serialize());
74 statement
.executeUpdate();
75 } catch (InvalidKeyException ignored
) {
77 } catch (SQLException e
) {
78 throw new RuntimeException("Failed update pre_key store", e
);
83 public boolean containsPreKey(int preKeyId
) {
84 return getPreKey(preKeyId
) != null;
88 public void removePreKey(int preKeyId
) {
92 WHERE p.account_id_type = ? AND p.key_id = ?
94 ).formatted(TABLE_PRE_KEY
);
95 try (final var connection
= database
.getConnection()) {
96 try (final var statement
= connection
.prepareStatement(sql
)) {
97 statement
.setInt(1, accountIdType
);
98 statement
.setInt(2, preKeyId
);
99 statement
.executeUpdate();
101 } catch (SQLException e
) {
102 throw new RuntimeException("Failed update pre_key store", e
);
106 public void removeAllPreKeys() {
110 WHERE p.account_id_type = ?
112 ).formatted(TABLE_PRE_KEY
);
113 try (final var connection
= database
.getConnection()) {
114 try (final var statement
= connection
.prepareStatement(sql
)) {
115 statement
.setInt(1, accountIdType
);
116 statement
.executeUpdate();
118 } catch (SQLException e
) {
119 throw new RuntimeException("Failed update pre_key store", e
);
123 void addLegacyPreKeys(final Collection
<PreKeyRecord
> preKeys
) {
124 logger
.debug("Migrating legacy preKeys to database");
125 long start
= System
.nanoTime();
128 INSERT INTO %s (account_id_type, key_id, public_key, private_key)
131 ).formatted(TABLE_PRE_KEY
);
132 try (final var connection
= database
.getConnection()) {
133 connection
.setAutoCommit(false);
134 final var deleteSql
= "DELETE FROM %s AS p WHERE p.account_id_type = ?".formatted(TABLE_PRE_KEY
);
135 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
136 statement
.setInt(1, accountIdType
);
137 statement
.executeUpdate();
139 try (final var statement
= connection
.prepareStatement(sql
)) {
140 for (final var record : preKeys
) {
141 statement
.setInt(1, accountIdType
);
142 statement
.setInt(2, record.getId());
143 final var keyPair
= record.getKeyPair();
144 statement
.setBytes(3, keyPair
.getPublicKey().serialize());
145 statement
.setBytes(4, keyPair
.getPrivateKey().serialize());
146 statement
.executeUpdate();
148 } catch (InvalidKeyException ignored
) {
151 } catch (SQLException e
) {
152 throw new RuntimeException("Failed update preKey store", e
);
154 logger
.debug("Complete preKeys migration took {}ms", (System
.nanoTime() - start
) / 1000000);
157 private PreKeyRecord
getPreKey(int preKeyId
) {
160 SELECT p.key_id, p.public_key, p.private_key
162 WHERE p.account_id_type = ? AND p.key_id = ?
164 ).formatted(TABLE_PRE_KEY
);
165 try (final var connection
= database
.getConnection()) {
166 try (final var statement
= connection
.prepareStatement(sql
)) {
167 statement
.setInt(1, accountIdType
);
168 statement
.setInt(2, preKeyId
);
169 return Utils
.executeQueryForOptional(statement
, this::getPreKeyRecordFromResultSet
).orElse(null);
171 } catch (SQLException e
) {
172 throw new RuntimeException("Failed read from pre_key store", e
);
176 private PreKeyRecord
getPreKeyRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
178 final var keyId
= resultSet
.getInt("key_id");
179 final var publicKey
= Curve
.decodePoint(resultSet
.getBytes("public_key"), 0);
180 final var privateKey
= Curve
.decodePrivatePoint(resultSet
.getBytes("private_key"));
181 return new PreKeyRecord(keyId
, new ECKeyPair(publicKey
, privateKey
));
182 } catch (InvalidKeyException e
) {
188 public void deleteAllStaleOneTimeEcPreKeys(final long threshold
, final int minCount
) {
192 WHERE p.account_id_type = ?1
193 AND p.stale_timestamp < ?2
197 WHERE p2.account_id_type = ?1
199 CASE WHEN p2.stale_timestamp IS NULL THEN 1 ELSE 0 END DESC,
200 p2.stale_timestamp DESC,
205 ).formatted(TABLE_PRE_KEY
, TABLE_PRE_KEY
);
206 try (final var connection
= database
.getConnection()) {
207 try (final var statement
= connection
.prepareStatement(sql
)) {
208 statement
.setInt(1, accountIdType
);
209 statement
.setLong(2, threshold
);
210 statement
.setInt(3, minCount
);
211 final var rowCount
= statement
.executeUpdate();
213 logger
.debug("Deleted {} stale one time pre keys", rowCount
);
216 } catch (SQLException e
) {
217 throw new RuntimeException("Failed update pre_key store", e
);
222 public void markAllOneTimeEcPreKeysStaleIfNecessary(final long staleTime
) {
226 SET stale_timestamp = ?
227 WHERE account_id_type = ? AND stale_timestamp IS NULL
229 ).formatted(TABLE_PRE_KEY
);
230 try (final var connection
= database
.getConnection()) {
231 try (final var statement
= connection
.prepareStatement(sql
)) {
232 statement
.setLong(1, staleTime
);
233 statement
.setInt(2, accountIdType
);
234 statement
.executeUpdate();
236 } catch (SQLException e
) {
237 throw new RuntimeException("Failed update pre_key store", e
);