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