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