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