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