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
.SignedPreKeyRecord
;
10 import org
.slf4j
.Logger
;
11 import org
.slf4j
.LoggerFactory
;
12 import org
.whispersystems
.signalservice
.api
.push
.ServiceIdType
;
14 import java
.sql
.Connection
;
15 import java
.sql
.ResultSet
;
16 import java
.sql
.SQLException
;
17 import java
.util
.Collection
;
18 import java
.util
.List
;
19 import java
.util
.Objects
;
21 public class SignedPreKeyStore
implements org
.signal
.libsignal
.protocol
.state
.SignedPreKeyStore
{
23 private static final String TABLE_SIGNED_PRE_KEY
= "signed_pre_key";
24 private final static Logger logger
= LoggerFactory
.getLogger(SignedPreKeyStore
.class);
26 private final Database database
;
27 private final int accountIdType
;
29 public static void createSql(Connection connection
) throws SQLException
{
30 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
31 try (final var statement
= connection
.createStatement()) {
32 statement
.executeUpdate("""
33 CREATE TABLE signed_pre_key (
34 _id INTEGER PRIMARY KEY,
35 account_id_type INTEGER NOT NULL,
36 key_id INTEGER NOT NULL,
37 public_key BLOB NOT NULL,
38 private_key BLOB NOT NULL,
39 signature BLOB NOT NULL,
40 timestamp INTEGER DEFAULT 0,
41 UNIQUE(account_id_type, key_id)
47 public SignedPreKeyStore(final Database database
, final ServiceIdType serviceIdType
) {
48 this.database
= database
;
49 this.accountIdType
= Utils
.getAccountIdType(serviceIdType
);
53 public SignedPreKeyRecord
loadSignedPreKey(int signedPreKeyId
) throws InvalidKeyIdException
{
54 final SignedPreKeyRecord signedPreKeyRecord
= getSignedPreKey(signedPreKeyId
);
55 if (signedPreKeyRecord
== null) {
56 throw new InvalidKeyIdException("No such signed pre key record!");
58 return signedPreKeyRecord
;
62 public List
<SignedPreKeyRecord
> loadSignedPreKeys() {
65 SELECT p.key_id, p.public_key, p.private_key, p.signature, p.timestamp
67 WHERE p.account_id_type = ?
69 ).formatted(TABLE_SIGNED_PRE_KEY
);
70 try (final var connection
= database
.getConnection()) {
71 try (final var statement
= connection
.prepareStatement(sql
)) {
72 statement
.setInt(1, accountIdType
);
73 return Utils
.executeQueryForStream(statement
, this::getSignedPreKeyRecordFromResultSet
)
74 .filter(Objects
::nonNull
)
77 } catch (SQLException e
) {
78 throw new RuntimeException("Failed read from signed_pre_key store", e
);
83 public void storeSignedPreKey(int signedPreKeyId
, SignedPreKeyRecord
record) {
86 INSERT INTO %s (account_id_type, key_id, public_key, private_key, signature, timestamp)
87 VALUES (?, ?, ?, ?, ?, ?)
89 ).formatted(TABLE_SIGNED_PRE_KEY
);
90 try (final var connection
= database
.getConnection()) {
91 try (final var statement
= connection
.prepareStatement(sql
)) {
92 statement
.setInt(1, accountIdType
);
93 statement
.setLong(2, signedPreKeyId
);
94 final var keyPair
= record.getKeyPair();
95 statement
.setBytes(3, keyPair
.getPublicKey().serialize());
96 statement
.setBytes(4, keyPair
.getPrivateKey().serialize());
97 statement
.setBytes(5, record.getSignature());
98 statement
.setLong(6, record.getTimestamp());
99 statement
.executeUpdate();
101 } catch (SQLException e
) {
102 throw new RuntimeException("Failed update signed_pre_key store", e
);
107 public boolean containsSignedPreKey(int signedPreKeyId
) {
108 return getSignedPreKey(signedPreKeyId
) != null;
112 public void removeSignedPreKey(int signedPreKeyId
) {
116 WHERE p.account_id_type = ? AND p.key_id = ?
118 ).formatted(TABLE_SIGNED_PRE_KEY
);
119 try (final var connection
= database
.getConnection()) {
120 try (final var statement
= connection
.prepareStatement(sql
)) {
121 statement
.setInt(1, accountIdType
);
122 statement
.setLong(2, signedPreKeyId
);
123 statement
.executeUpdate();
125 } catch (SQLException e
) {
126 throw new RuntimeException("Failed update signed_pre_key store", e
);
130 public void removeAllSignedPreKeys() {
134 WHERE p.account_id_type = ?
136 ).formatted(TABLE_SIGNED_PRE_KEY
);
137 try (final var connection
= database
.getConnection()) {
138 try (final var statement
= connection
.prepareStatement(sql
)) {
139 statement
.setInt(1, accountIdType
);
140 statement
.executeUpdate();
142 } catch (SQLException e
) {
143 throw new RuntimeException("Failed update signed_pre_key store", e
);
147 void addLegacySignedPreKeys(final Collection
<SignedPreKeyRecord
> signedPreKeys
) {
148 logger
.debug("Migrating legacy signedPreKeys to database");
149 long start
= System
.nanoTime();
152 INSERT INTO %s (account_id_type, key_id, public_key, private_key, signature, timestamp)
153 VALUES (?, ?, ?, ?, ?, ?)
155 ).formatted(TABLE_SIGNED_PRE_KEY
);
156 try (final var connection
= database
.getConnection()) {
157 connection
.setAutoCommit(false);
158 final var deleteSql
= "DELETE FROM %s AS p WHERE p.account_id_type = ?".formatted(TABLE_SIGNED_PRE_KEY
);
159 try (final var statement
= connection
.prepareStatement(deleteSql
)) {
160 statement
.setInt(1, accountIdType
);
161 statement
.executeUpdate();
163 try (final var statement
= connection
.prepareStatement(sql
)) {
164 for (final var record : signedPreKeys
) {
165 statement
.setInt(1, accountIdType
);
166 statement
.setLong(2, record.getId());
167 final var keyPair
= record.getKeyPair();
168 statement
.setBytes(3, keyPair
.getPublicKey().serialize());
169 statement
.setBytes(4, keyPair
.getPrivateKey().serialize());
170 statement
.setBytes(5, record.getSignature());
171 statement
.setLong(6, record.getTimestamp());
172 statement
.executeUpdate();
176 } catch (SQLException e
) {
177 throw new RuntimeException("Failed update signedPreKey store", e
);
179 logger
.debug("Complete signedPreKeys migration took {}ms", (System
.nanoTime() - start
) / 1000000);
182 private SignedPreKeyRecord
getSignedPreKey(int signedPreKeyId
) {
185 SELECT p.key_id, p.public_key, p.private_key, p.signature, p.timestamp
187 WHERE p.account_id_type = ? AND p.key_id = ?
189 ).formatted(TABLE_SIGNED_PRE_KEY
);
190 try (final var connection
= database
.getConnection()) {
191 try (final var statement
= connection
.prepareStatement(sql
)) {
192 statement
.setInt(1, accountIdType
);
193 statement
.setLong(2, signedPreKeyId
);
194 return Utils
.executeQueryForOptional(statement
, this::getSignedPreKeyRecordFromResultSet
).orElse(null);
196 } catch (SQLException e
) {
197 throw new RuntimeException("Failed read from signed_pre_key store", e
);
201 private SignedPreKeyRecord
getSignedPreKeyRecordFromResultSet(ResultSet resultSet
) throws SQLException
{
203 final var keyId
= resultSet
.getInt("key_id");
204 final var publicKey
= Curve
.decodePoint(resultSet
.getBytes("public_key"), 0);
205 final var privateKey
= Curve
.decodePrivatePoint(resultSet
.getBytes("private_key"));
206 final var signature
= resultSet
.getBytes("signature");
207 final var timestamp
= resultSet
.getLong("timestamp");
208 return new SignedPreKeyRecord(keyId
, timestamp
, new ECKeyPair(publicKey
, privateKey
), signature
);
209 } catch (InvalidKeyException e
) {