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
.InvalidKeyIdException
;
6 import org
.signal
.libsignal
.protocol
.InvalidMessageException
;
7 import org
.signal
.libsignal
.protocol
.state
.KyberPreKeyRecord
;
8 import org
.slf4j
.Logger
;
9 import org
.slf4j
.LoggerFactory
;
10 import org
.whispersystems
.signalservice
.api
.SignalServiceKyberPreKeyStore
;
11 import org
.whispersystems
.signalservice
.api
.push
.ServiceIdType
;
13 import java
.sql
.Connection
;
14 import java
.sql
.ResultSet
;
15 import java
.sql
.SQLException
;
16 import java
.util
.List
;
18 import static org
.asamk
.signal
.manager
.config
.ServiceConfig
.PREKEY_ARCHIVE_AGE
;
20 public class KyberPreKeyStore
implements SignalServiceKyberPreKeyStore
{
22 private static final String TABLE_KYBER_PRE_KEY
= "kyber_pre_key";
23 private final static Logger logger
= LoggerFactory
.getLogger(KyberPreKeyStore
.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 kyber_pre_key (
33 _id INTEGER PRIMARY KEY,
34 account_id_type INTEGER NOT NULL,
35 key_id INTEGER NOT NULL,
36 serialized BLOB NOT NULL,
37 is_last_resort INTEGER NOT NULL,
38 stale_timestamp INTEGER,
39 timestamp INTEGER DEFAULT 0,
40 UNIQUE(account_id_type, key_id)
46 public KyberPreKeyStore(final Database database
, final ServiceIdType serviceIdType
) {
47 this.database
= database
;
48 this.accountIdType
= Utils
.getAccountIdType(serviceIdType
);
52 public KyberPreKeyRecord
loadKyberPreKey(final int keyId
) throws InvalidKeyIdException
{
53 final var kyberPreKey
= getPreKey(keyId
);
54 if (kyberPreKey
== null) {
55 throw new InvalidKeyIdException("No such kyber pre key record: " + keyId
);
61 public List
<KyberPreKeyRecord
> loadKyberPreKeys() {
66 WHERE p.account_id_type = ?
68 ).formatted(TABLE_KYBER_PRE_KEY
);
69 try (final var connection
= database
.getConnection()) {
70 try (final var statement
= connection
.prepareStatement(sql
)) {
71 statement
.setInt(1, accountIdType
);
72 return Utils
.executeQueryForStream(statement
, this::getKyberPreKeyRecordFromResultSet
).toList();
74 } catch (SQLException e
) {
75 throw new RuntimeException("Failed read from kyber_pre_key store", e
);
80 public List
<KyberPreKeyRecord
> loadLastResortKyberPreKeys() {
85 WHERE p.account_id_type = ? AND p.is_last_resort = TRUE
87 ).formatted(TABLE_KYBER_PRE_KEY
);
88 try (final var connection
= database
.getConnection()) {
89 try (final var statement
= connection
.prepareStatement(sql
)) {
90 statement
.setInt(1, accountIdType
);
91 return Utils
.executeQueryForStream(statement
, this::getKyberPreKeyRecordFromResultSet
).toList();
93 } catch (SQLException e
) {
94 throw new RuntimeException("Failed read from kyber_pre_key store", e
);
99 public void storeLastResortKyberPreKey(final int keyId
, final KyberPreKeyRecord
record) {
100 storeKyberPreKey(keyId
, record, true);
104 public void storeKyberPreKey(final int keyId
, final KyberPreKeyRecord
record) {
105 storeKyberPreKey(keyId
, record, false);
108 public void storeKyberPreKey(final int keyId
, final KyberPreKeyRecord
record, final boolean isLastResort
) {
111 INSERT INTO %s (account_id_type, key_id, serialized, is_last_resort, timestamp)
112 VALUES (?, ?, ?, ?, ?)
114 ).formatted(TABLE_KYBER_PRE_KEY
);
115 try (final var connection
= database
.getConnection()) {
116 try (final var statement
= connection
.prepareStatement(sql
)) {
117 statement
.setInt(1, accountIdType
);
118 statement
.setInt(2, keyId
);
119 statement
.setBytes(3, record.serialize());
120 statement
.setBoolean(4, isLastResort
);
121 statement
.setLong(5, record.getTimestamp());
122 statement
.executeUpdate();
124 } catch (SQLException e
) {
125 throw new RuntimeException("Failed update kyber_pre_key store", e
);
130 public boolean containsKyberPreKey(final int keyId
) {
131 return getPreKey(keyId
) != null;
135 public void markKyberPreKeyUsed(final int keyId
) {
139 WHERE p.account_id_type = ? AND p.key_id = ? AND p.is_last_resort = FALSE
141 ).formatted(TABLE_KYBER_PRE_KEY
);
142 try (final var connection
= database
.getConnection()) {
143 try (final var statement
= connection
.prepareStatement(sql
)) {
144 statement
.setInt(1, accountIdType
);
145 statement
.setInt(2, keyId
);
146 statement
.executeUpdate();
148 } catch (SQLException e
) {
149 throw new RuntimeException("Failed update kyber_pre_key store", e
);
154 public void removeKyberPreKey(final int keyId
) {
158 WHERE p.account_id_type = ? AND p.key_id = ?
160 ).formatted(TABLE_KYBER_PRE_KEY
);
161 try (final var connection
= database
.getConnection()) {
162 try (final var statement
= connection
.prepareStatement(sql
)) {
163 statement
.setInt(1, accountIdType
);
164 statement
.setInt(2, keyId
);
165 statement
.executeUpdate();
167 } catch (SQLException e
) {
168 throw new RuntimeException("Failed update kyber_pre_key store", e
);
172 public void removeAllKyberPreKeys() {
176 WHERE p.account_id_type = ?
178 ).formatted(TABLE_KYBER_PRE_KEY
);
179 try (final var connection
= database
.getConnection()) {
180 try (final var statement
= connection
.prepareStatement(sql
)) {
181 statement
.setInt(1, accountIdType
);
182 statement
.executeUpdate();
184 } catch (SQLException e
) {
185 throw new RuntimeException("Failed update kyber_pre_key store", e
);
189 private KyberPreKeyRecord
getPreKey(int keyId
) {
194 WHERE p.account_id_type = ? AND p.key_id = ?
196 ).formatted(TABLE_KYBER_PRE_KEY
);
197 try (final var connection
= database
.getConnection()) {
198 try (final var statement
= connection
.prepareStatement(sql
)) {
199 statement
.setInt(1, accountIdType
);
200 statement
.setInt(2, keyId
);
201 return Utils
.executeQueryForOptional(statement
, this::getKyberPreKeyRecordFromResultSet
).orElse(null);
203 } catch (SQLException e
) {
204 throw new RuntimeException("Failed read from kyber_pre_key store", e
);
208 private KyberPreKeyRecord
getKyberPreKeyRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
210 final var serialized
= resultSet
.getBytes("serialized");
211 return new KyberPreKeyRecord(serialized
);
212 } catch (InvalidMessageException e
) {
217 public void removeOldLastResortKyberPreKeys(int activeLastResortKyberPreKeyId
) {
224 WHERE p.account_id_type = ?
225 AND p.is_last_resort = TRUE
228 ORDER BY p.timestamp DESC
232 ).formatted(TABLE_KYBER_PRE_KEY
, TABLE_KYBER_PRE_KEY
);
233 try (final var connection
= database
.getConnection()) {
234 try (final var statement
= connection
.prepareStatement(sql
)) {
235 statement
.setInt(1, accountIdType
);
236 statement
.setInt(2, activeLastResortKyberPreKeyId
);
237 statement
.setLong(3, System
.currentTimeMillis() - PREKEY_ARCHIVE_AGE
);
238 statement
.executeUpdate();
240 } catch (SQLException e
) {
241 throw new RuntimeException("Failed update kyber_pre_key store", e
);
246 public void deleteAllStaleOneTimeKyberPreKeys(final long threshold
, final int minCount
) {
250 WHERE p.account_id_type = ?1
251 AND p.stale_timestamp < ?2
252 AND p.is_last_resort = FALSE
256 WHERE p2.account_id_type = ?1
258 CASE WHEN p2.stale_timestamp IS NULL THEN 1 ELSE 0 END DESC,
259 p2.stale_timestamp DESC,
264 ).formatted(TABLE_KYBER_PRE_KEY
, TABLE_KYBER_PRE_KEY
);
265 try (final var connection
= database
.getConnection()) {
266 try (final var statement
= connection
.prepareStatement(sql
)) {
267 statement
.setInt(1, accountIdType
);
268 statement
.setLong(2, threshold
);
269 statement
.setInt(3, minCount
);
270 final var rowCount
= statement
.executeUpdate();
272 logger
.debug("Deleted {} stale one time kyber pre keys", rowCount
);
275 } catch (SQLException e
) {
276 throw new RuntimeException("Failed update kyber_pre_key store", e
);
281 public void markAllOneTimeKyberPreKeysStaleIfNecessary(final long staleTime
) {
285 SET stale_timestamp = ?
286 WHERE account_id_type = ? AND stale_timestamp IS NULL AND is_last_resort = FALSE
288 ).formatted(TABLE_KYBER_PRE_KEY
);
289 try (final var connection
= database
.getConnection()) {
290 try (final var statement
= connection
.prepareStatement(sql
)) {
291 statement
.setLong(1, staleTime
);
292 statement
.setInt(2, accountIdType
);
293 statement
.executeUpdate();
295 } catch (SQLException e
) {
296 throw new RuntimeException("Failed update kyber_pre_key store", e
);