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