1 package org
.asamk
.signal
.manager
.storage
.identities
;
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
;
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
;
20 import io
.reactivex
.rxjava3
.core
.Observable
;
21 import io
.reactivex
.rxjava3
.subjects
.PublishSubject
;
23 public class IdentityKeyStore
{
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();
31 private boolean isRetryingDecryption
= false;
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
48 public IdentityKeyStore(final Database database
, final TrustNewIdentity trustNewIdentity
) {
49 this.database
= database
;
50 this.trustNewIdentity
= trustNewIdentity
;
53 public Observable
<ServiceId
> getIdentityChanges() {
54 return identityChanges
;
57 public boolean saveIdentity(final ServiceId serviceId
, final IdentityKey identityKey
) {
58 if (isRetryingDecryption
) {
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
);
69 saveNewIdentity(connection
, serviceId
, identityKey
, identityInfo
== null);
71 } catch (SQLException e
) {
72 throw new RuntimeException("Failed update identity store", e
);
76 public void setRetryingDecryption(final boolean retryingDecryption
) {
77 isRetryingDecryption
= retryingDecryption
;
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
);
87 if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
88 logger
.debug("Not updating trust level for recipient {}, different identity found", serviceId
);
91 if (identityInfo
.getTrustLevel() == trustLevel
) {
92 logger
.trace("Not updating trust level for recipient {}, trust level already matches", serviceId
);
96 logger
.debug("Updating trust level for recipient {} with trust {}", serviceId
, trustLevel
);
97 final var newIdentityInfo
= new IdentityInfo(serviceId
,
100 identityInfo
.getDateAddedTimestamp());
101 storeIdentity(connection
, newIdentityInfo
);
103 } catch (SQLException e
) {
104 throw new RuntimeException("Failed update identity store", e
);
108 public boolean isTrustedIdentity(ServiceId serviceId
, IdentityKey identityKey
, Direction direction
) {
109 if (trustNewIdentity
== TrustNewIdentity
.ALWAYS
) {
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
);
127 logger
.trace("Trusting identity for {} for {}: {}", serviceId
, direction
, false);
132 final var isTrusted
= identityInfo
!= null && identityInfo
.isTrusted();
133 logger
.trace("Trusting identity for {} for {}: {}", serviceId
, direction
, isTrusted
);
135 } catch (SQLException e
) {
136 throw new RuntimeException("Failed read from identity store", e
);
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
);
148 public List
<IdentityInfo
> getIdentities() {
149 try (final var connection
= database
.getConnection()) {
152 SELECT i.uuid, i.identity_key, i.added_timestamp, i.trust_level
155 ).formatted(TABLE_IDENTITY
);
156 try (final var statement
= connection
.prepareStatement(sql
)) {
157 return Utils
.executeQueryForStream(statement
, this::getIdentityInfoFromResultSet
)
158 .filter(Objects
::nonNull
)
161 } catch (SQLException e
) {
162 throw new RuntimeException("Failed read from identity store", e
);
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
);
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
);
183 } catch (SQLException e
) {
184 throw new RuntimeException("Failed update identity store", e
);
186 logger
.debug("Complete identities migration took {}ms", (System
.nanoTime() - start
) / 1000000);
189 private IdentityInfo
loadIdentity(
190 final Connection connection
, final ServiceId serviceId
191 ) throws SQLException
{
194 SELECT i.uuid, i.identity_key, i.added_timestamp, i.trust_level
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);
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
);
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());
227 INSERT OR REPLACE INTO %s (uuid, identity_key, added_timestamp, trust_level)
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();
240 private void deleteIdentity(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
246 ).formatted(TABLE_IDENTITY
);
247 try (final var statement
= connection
.prepareStatement(sql
)) {
248 statement
.setBytes(1, serviceId
.toByteArray());
249 statement
.executeUpdate();
253 private IdentityInfo
getIdentityInfoFromResultSet(ResultSet resultSet
) throws SQLException
{
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");
260 return new IdentityInfo(serviceId
, id
, trustLevel
, added
);
261 } catch (InvalidKeyException e
) {
262 logger
.warn("Failed to load identity key, resetting: {}", e
.getMessage());