]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/prekeys/PreKeyStore.java
d2d557108172d2db1fa397570be35edbd16d8cd4
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / prekeys / PreKeyStore.java
1 package org.asamk.signal.manager.storage.prekeys;
2
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;
14
15 import java.sql.Connection;
16 import java.sql.ResultSet;
17 import java.sql.SQLException;
18 import java.util.Collection;
19
20 public class PreKeyStore implements SignalServicePreKeyStore {
21
22 private static final String TABLE_PRE_KEY = "pre_key";
23 private static final Logger logger = LoggerFactory.getLogger(PreKeyStore.class);
24
25 private final Database database;
26 private final int accountIdType;
27
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)
40 ) STRICT;
41 """);
42 }
43 }
44
45 public PreKeyStore(final Database database, final ServiceIdType serviceIdType) {
46 this.database = database;
47 this.accountIdType = Utils.getAccountIdType(serviceIdType);
48 }
49
50 @Override
51 public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
52 final var preKey = getPreKey(preKeyId);
53 if (preKey == null) {
54 throw new InvalidKeyIdException("No such pre key record: " + preKeyId);
55 }
56 return preKey;
57 }
58
59 @Override
60 public void storePreKey(int preKeyId, PreKeyRecord record) {
61 final var sql = (
62 """
63 INSERT INTO %s (account_id_type, key_id, public_key, private_key)
64 VALUES (?, ?, ?, ?)
65 """
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) {
76 }
77 } catch (SQLException e) {
78 throw new RuntimeException("Failed update pre_key store", e);
79 }
80 }
81
82 @Override
83 public boolean containsPreKey(int preKeyId) {
84 return getPreKey(preKeyId) != null;
85 }
86
87 @Override
88 public void removePreKey(int preKeyId) {
89 final var sql = (
90 """
91 DELETE FROM %s AS p
92 WHERE p.account_id_type = ? AND p.key_id = ?
93 """
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();
100 }
101 } catch (SQLException e) {
102 throw new RuntimeException("Failed update pre_key store", e);
103 }
104 }
105
106 public void removeAllPreKeys() {
107 final var sql = (
108 """
109 DELETE FROM %s AS p
110 WHERE p.account_id_type = ?
111 """
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();
117 }
118 } catch (SQLException e) {
119 throw new RuntimeException("Failed update pre_key store", e);
120 }
121 }
122
123 void addLegacyPreKeys(final Collection<PreKeyRecord> preKeys) {
124 logger.debug("Migrating legacy preKeys to database");
125 long start = System.nanoTime();
126 final var sql = (
127 """
128 INSERT INTO %s (account_id_type, key_id, public_key, private_key)
129 VALUES (?, ?, ?, ?)
130 """
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();
138 }
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();
147 }
148 } catch (InvalidKeyException ignored) {
149 }
150 connection.commit();
151 } catch (SQLException e) {
152 throw new RuntimeException("Failed update preKey store", e);
153 }
154 logger.debug("Complete preKeys migration took {}ms", (System.nanoTime() - start) / 1000000);
155 }
156
157 private PreKeyRecord getPreKey(int preKeyId) {
158 final var sql = (
159 """
160 SELECT p.key_id, p.public_key, p.private_key
161 FROM %s p
162 WHERE p.account_id_type = ? AND p.key_id = ?
163 """
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);
170 }
171 } catch (SQLException e) {
172 throw new RuntimeException("Failed read from pre_key store", e);
173 }
174 }
175
176 private PreKeyRecord getPreKeyRecordFromResultSet(ResultSet resultSet) throws SQLException {
177 try {
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) {
183 return null;
184 }
185 }
186
187 @Override
188 public void deleteAllStaleOneTimeEcPreKeys(final long threshold, final int minCount) {
189 final var sql = (
190 """
191 DELETE FROM %s AS p
192 WHERE p.account_id_type = ?1
193 AND p.stale_timestamp < ?2
194 AND p._id NOT IN (
195 SELECT _id
196 FROM %s AS p2
197 WHERE p2.account_id_type = ?1
198 ORDER BY
199 CASE WHEN p2.stale_timestamp IS NULL THEN 1 ELSE 0 END DESC,
200 p2.stale_timestamp DESC,
201 p2._id DESC
202 LIMIT ?3
203 )
204 """
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();
212 if (rowCount > 0) {
213 logger.debug("Deleted {} stale one time pre keys", rowCount);
214 }
215 }
216 } catch (SQLException e) {
217 throw new RuntimeException("Failed update pre_key store", e);
218 }
219 }
220
221 @Override
222 public void markAllOneTimeEcPreKeysStaleIfNecessary(final long staleTime) {
223 final var sql = (
224 """
225 UPDATE %s
226 SET stale_timestamp = ?
227 WHERE account_id_type = ? AND stale_timestamp IS NULL
228 """
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();
235 }
236 } catch (SQLException e) {
237 throw new RuntimeException("Failed update pre_key store", e);
238 }
239 }
240 }