]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/identities/IdentityKeyStore.java
5e32ba125811f8e90e46d68480524974e8f7fb92
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / identities / IdentityKeyStore.java
1 package org.asamk.signal.manager.storage.identities;
2
3 import org.asamk.signal.manager.api.TrustLevel;
4 import org.asamk.signal.manager.api.TrustNewIdentity;
5 import org.asamk.signal.manager.storage.Database;
6 import org.asamk.signal.manager.storage.Utils;
7 import org.asamk.signal.manager.storage.recipients.RecipientStore;
8 import org.signal.libsignal.protocol.IdentityKey;
9 import org.signal.libsignal.protocol.InvalidKeyException;
10 import org.signal.libsignal.protocol.state.IdentityKeyStore.Direction;
11 import org.slf4j.Logger;
12 import org.slf4j.LoggerFactory;
13 import org.whispersystems.signalservice.api.push.ServiceId;
14
15 import java.sql.Connection;
16 import java.sql.ResultSet;
17 import java.sql.SQLException;
18 import java.util.Collection;
19 import java.util.List;
20 import java.util.Objects;
21
22 import io.reactivex.rxjava3.core.Observable;
23 import io.reactivex.rxjava3.subjects.PublishSubject;
24
25 public class IdentityKeyStore {
26
27 private static final Logger logger = LoggerFactory.getLogger(IdentityKeyStore.class);
28 private static final String TABLE_IDENTITY = "identity";
29 private final Database database;
30 private final TrustNewIdentity trustNewIdentity;
31 private final RecipientStore recipientStore;
32 private final PublishSubject<ServiceId> identityChanges = PublishSubject.create();
33
34 private boolean isRetryingDecryption = false;
35
36 public static void createSql(Connection connection) throws SQLException {
37 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
38 try (final var statement = connection.createStatement()) {
39 statement.executeUpdate("""
40 CREATE TABLE identity (
41 _id INTEGER PRIMARY KEY,
42 address TEXT UNIQUE NOT NULL,
43 identity_key BLOB NOT NULL,
44 added_timestamp INTEGER NOT NULL,
45 trust_level INTEGER NOT NULL
46 ) STRICT;
47 """);
48 }
49 }
50
51 public IdentityKeyStore(
52 final Database database,
53 final TrustNewIdentity trustNewIdentity,
54 RecipientStore recipientStore
55 ) {
56 this.database = database;
57 this.trustNewIdentity = trustNewIdentity;
58 this.recipientStore = recipientStore;
59 }
60
61 public Observable<ServiceId> getIdentityChanges() {
62 return identityChanges;
63 }
64
65 public boolean saveIdentity(final ServiceId serviceId, final IdentityKey identityKey) {
66 return saveIdentity(serviceId.toString(), identityKey);
67 }
68
69 public boolean saveIdentity(
70 final Connection connection,
71 final ServiceId serviceId,
72 final IdentityKey identityKey
73 ) throws SQLException {
74 return saveIdentity(connection, serviceId.toString(), identityKey);
75 }
76
77 boolean saveIdentity(final String address, final IdentityKey identityKey) {
78 if (isRetryingDecryption) {
79 return false;
80 }
81 try (final var connection = database.getConnection()) {
82 return saveIdentity(connection, address, identityKey);
83 } catch (SQLException e) {
84 throw new RuntimeException("Failed update identity store", e);
85 }
86 }
87
88 private boolean saveIdentity(
89 final Connection connection,
90 final String address,
91 final IdentityKey identityKey
92 ) throws SQLException {
93 final var identityInfo = loadIdentity(connection, address);
94 if (identityInfo != null && identityInfo.getIdentityKey().equals(identityKey)) {
95 // Identity already exists, not updating the trust level
96 logger.trace("Not storing new identity for recipient {}, identity already stored", address);
97 return false;
98 }
99
100 saveNewIdentity(connection, address, identityKey, identityInfo == null);
101 return true;
102 }
103
104 public void setRetryingDecryption(final boolean retryingDecryption) {
105 isRetryingDecryption = retryingDecryption;
106 }
107
108 public boolean setIdentityTrustLevel(ServiceId serviceId, IdentityKey identityKey, TrustLevel trustLevel) {
109 try (final var connection = database.getConnection()) {
110 return setIdentityTrustLevel(connection, serviceId, identityKey, trustLevel);
111 } catch (SQLException e) {
112 throw new RuntimeException("Failed update identity store", e);
113 }
114 }
115
116 public boolean setIdentityTrustLevel(
117 final Connection connection,
118 final ServiceId serviceId,
119 final IdentityKey identityKey,
120 final TrustLevel trustLevel
121 ) throws SQLException {
122 final var address = serviceId.toString();
123 final var identityInfo = loadIdentity(connection, address);
124 if (identityInfo == null) {
125 logger.debug("Not updating trust level for recipient {}, identity not found", serviceId);
126 return false;
127 }
128 if (!identityInfo.getIdentityKey().equals(identityKey)) {
129 logger.debug("Not updating trust level for recipient {}, different identity found", serviceId);
130 return false;
131 }
132 if (identityInfo.getTrustLevel() == trustLevel) {
133 logger.trace("Not updating trust level for recipient {}, trust level already matches", serviceId);
134 return false;
135 }
136
137 logger.debug("Updating trust level for recipient {} with trust {}", serviceId, trustLevel);
138 final var newIdentityInfo = new IdentityInfo(address,
139 identityKey,
140 trustLevel,
141 identityInfo.getDateAddedTimestamp());
142 storeIdentity(connection, newIdentityInfo);
143 return true;
144 }
145
146 public boolean isTrustedIdentity(ServiceId serviceId, IdentityKey identityKey, Direction direction) {
147 return isTrustedIdentity(serviceId.toString(), identityKey, direction);
148 }
149
150 public boolean isTrustedIdentity(String address, IdentityKey identityKey, Direction direction) {
151 if (trustNewIdentity == TrustNewIdentity.ALWAYS) {
152 return true;
153 }
154
155 try (final var connection = database.getConnection()) {
156 // TODO implement possibility for different handling of incoming/outgoing trust decisions
157 var identityInfo = loadIdentity(connection, address);
158 if (identityInfo == null) {
159 logger.debug("Initial identity found for {}, saving.", address);
160 saveNewIdentity(connection, address, identityKey, true);
161 identityInfo = loadIdentity(connection, address);
162 } else if (!identityInfo.getIdentityKey().equals(identityKey)) {
163 // Identity found, but different
164 if (direction == Direction.SENDING) {
165 logger.debug("Changed identity found for {}, saving.", address);
166 saveNewIdentity(connection, address, identityKey, false);
167 identityInfo = loadIdentity(connection, address);
168 } else {
169 logger.trace("Trusting identity for {} for {}: {}", address, direction, false);
170 return false;
171 }
172 }
173
174 final var isTrusted = identityInfo != null && identityInfo.isTrusted();
175 logger.trace("Trusting identity for {} for {}: {}", address, direction, isTrusted);
176 return isTrusted;
177 } catch (SQLException e) {
178 throw new RuntimeException("Failed read from identity store", e);
179 }
180 }
181
182 public IdentityInfo getIdentityInfo(ServiceId serviceId) {
183 return getIdentityInfo(serviceId.toString());
184 }
185
186 public IdentityInfo getIdentityInfo(String address) {
187 try (final var connection = database.getConnection()) {
188 return loadIdentity(connection, address);
189 } catch (SQLException e) {
190 throw new RuntimeException("Failed read from identity store", e);
191 }
192 }
193
194 public IdentityInfo getIdentityInfo(Connection connection, String address) throws SQLException {
195 return loadIdentity(connection, address);
196 }
197
198 public List<IdentityInfo> getIdentities() {
199 try (final var connection = database.getConnection()) {
200 final var sql = (
201 """
202 SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level
203 FROM %s AS i
204 """
205 ).formatted(TABLE_IDENTITY);
206 try (final var statement = connection.prepareStatement(sql)) {
207 return Utils.executeQueryForStream(statement, this::getIdentityInfoFromResultSet)
208 .filter(Objects::nonNull)
209 .toList();
210 }
211 } catch (SQLException e) {
212 throw new RuntimeException("Failed read from identity store", e);
213 }
214 }
215
216 public void deleteIdentity(final ServiceId serviceId) {
217 try (final var connection = database.getConnection()) {
218 deleteIdentity(connection, serviceId.toString());
219 } catch (SQLException e) {
220 throw new RuntimeException("Failed update identity store", e);
221 }
222 }
223
224 void addLegacyIdentities(final Collection<IdentityInfo> identities) {
225 logger.debug("Migrating legacy identities to database");
226 long start = System.nanoTime();
227 try (final var connection = database.getConnection()) {
228 connection.setAutoCommit(false);
229 for (final var identityInfo : identities) {
230 storeIdentity(connection, identityInfo);
231 }
232 connection.commit();
233 } catch (SQLException e) {
234 throw new RuntimeException("Failed update identity store", e);
235 }
236 logger.debug("Complete identities migration took {}ms", (System.nanoTime() - start) / 1000000);
237 }
238
239 private IdentityInfo loadIdentity(final Connection connection, final String address) throws SQLException {
240 final var sql = (
241 """
242 SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level
243 FROM %s AS i
244 WHERE i.address = ?
245 """
246 ).formatted(TABLE_IDENTITY);
247 try (final var statement = connection.prepareStatement(sql)) {
248 statement.setString(1, address);
249 return Utils.executeQueryForOptional(statement, this::getIdentityInfoFromResultSet).orElse(null);
250 }
251 }
252
253 private void saveNewIdentity(
254 final Connection connection,
255 final String address,
256 final IdentityKey identityKey,
257 final boolean firstIdentity
258 ) throws SQLException {
259 final var trustLevel = trustNewIdentity == TrustNewIdentity.ALWAYS || (
260 trustNewIdentity == TrustNewIdentity.ON_FIRST_USE && firstIdentity
261 ) ? TrustLevel.TRUSTED_UNVERIFIED : TrustLevel.UNTRUSTED;
262 logger.debug("Storing new identity for recipient {} with trust {}", address, trustLevel);
263 final var newIdentityInfo = new IdentityInfo(address, identityKey, trustLevel, System.currentTimeMillis());
264 storeIdentity(connection, newIdentityInfo);
265 final var serviceId = ServiceId.parseOrNull(address);
266 if (serviceId != null) {
267 identityChanges.onNext(serviceId);
268 }
269 }
270
271 private void storeIdentity(final Connection connection, final IdentityInfo identityInfo) throws SQLException {
272 logger.trace("Storing identity info for {}, trust: {}, added: {}",
273 identityInfo.getServiceId(),
274 identityInfo.getTrustLevel(),
275 identityInfo.getDateAddedTimestamp());
276 final var sql = (
277 """
278 INSERT OR REPLACE INTO %s (address, identity_key, added_timestamp, trust_level)
279 VALUES (?, ?, ?, ?)
280 """
281 ).formatted(TABLE_IDENTITY);
282 try (final var statement = connection.prepareStatement(sql)) {
283 statement.setString(1, identityInfo.getAddress());
284 statement.setBytes(2, identityInfo.getIdentityKey().serialize());
285 statement.setLong(3, identityInfo.getDateAddedTimestamp());
286 statement.setInt(4, identityInfo.getTrustLevel().ordinal());
287 statement.executeUpdate();
288 }
289 recipientStore.rotateStorageId(connection, identityInfo.getServiceId());
290 }
291
292 private void deleteIdentity(final Connection connection, final String address) throws SQLException {
293 final var sql = (
294 """
295 DELETE FROM %s AS i
296 WHERE i.address = ?
297 """
298 ).formatted(TABLE_IDENTITY);
299 try (final var statement = connection.prepareStatement(sql)) {
300 statement.setString(1, address);
301 statement.executeUpdate();
302 }
303 }
304
305 private IdentityInfo getIdentityInfoFromResultSet(ResultSet resultSet) throws SQLException {
306 try {
307 final var address = resultSet.getString("address");
308 final var id = new IdentityKey(resultSet.getBytes("identity_key"));
309 final var trustLevel = TrustLevel.fromInt(resultSet.getInt("trust_level"));
310 final var added = resultSet.getLong("added_timestamp");
311
312 return new IdentityInfo(address, id, trustLevel, added);
313 } catch (InvalidKeyException e) {
314 logger.warn("Failed to load identity key, resetting: {}", e.getMessage());
315 return null;
316 }
317 }
318 }