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
;
19 import io
.reactivex
.rxjava3
.core
.Observable
;
20 import io
.reactivex
.rxjava3
.subjects
.PublishSubject
;
22 public class IdentityKeyStore
{
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();
30 private boolean isRetryingDecryption
= false;
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
47 public IdentityKeyStore(final Database database
, final TrustNewIdentity trustNewIdentity
) {
48 this.database
= database
;
49 this.trustNewIdentity
= trustNewIdentity
;
52 public Observable
<ServiceId
> getIdentityChanges() {
53 return identityChanges
;
56 public boolean saveIdentity(final ServiceId serviceId
, final IdentityKey identityKey
) {
57 if (isRetryingDecryption
) {
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
);
68 saveNewIdentity(connection
, serviceId
, identityKey
, identityInfo
== null);
70 } catch (SQLException e
) {
71 throw new RuntimeException("Failed update identity store", e
);
75 public void setRetryingDecryption(final boolean retryingDecryption
) {
76 isRetryingDecryption
= retryingDecryption
;
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
);
86 if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
87 logger
.debug("Not updating trust level for recipient {}, different identity found", serviceId
);
90 if (identityInfo
.getTrustLevel() == trustLevel
) {
91 logger
.trace("Not updating trust level for recipient {}, trust level already matches", serviceId
);
95 logger
.debug("Updating trust level for recipient {} with trust {}", serviceId
, trustLevel
);
96 final var newIdentityInfo
= new IdentityInfo(serviceId
,
99 identityInfo
.getDateAddedTimestamp());
100 storeIdentity(connection
, newIdentityInfo
);
102 } catch (SQLException e
) {
103 throw new RuntimeException("Failed update identity store", e
);
107 public boolean isTrustedIdentity(ServiceId serviceId
, IdentityKey identityKey
, Direction direction
) {
108 if (trustNewIdentity
== TrustNewIdentity
.ALWAYS
) {
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
);
126 logger
.trace("Trusting identity for {} for {}: {}", serviceId
, direction
, false);
131 final var isTrusted
= identityInfo
!= null && identityInfo
.isTrusted();
132 logger
.trace("Trusting identity for {} for {}: {}", serviceId
, direction
, isTrusted
);
134 } catch (SQLException e
) {
135 throw new RuntimeException("Failed read from identity store", e
);
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
);
147 public List
<IdentityInfo
> getIdentities() {
148 try (final var connection
= database
.getConnection()) {
151 SELECT i.uuid, i.identity_key, i.added_timestamp, i.trust_level
154 ).formatted(TABLE_IDENTITY
);
155 try (final var statement
= connection
.prepareStatement(sql
)) {
156 return Utils
.executeQueryForStream(statement
, this::getIdentityInfoFromResultSet
).toList();
158 } catch (SQLException e
) {
159 throw new RuntimeException("Failed read from identity store", e
);
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
);
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
);
180 } catch (SQLException e
) {
181 throw new RuntimeException("Failed update identity store", e
);
183 logger
.debug("Complete identities migration took {}ms", (System
.nanoTime() - start
) / 1000000);
186 private IdentityInfo
loadIdentity(
187 final Connection connection
, final ServiceId serviceId
188 ) throws SQLException
{
191 SELECT i.uuid, i.identity_key, i.added_timestamp, i.trust_level
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);
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
);
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());
224 INSERT OR REPLACE INTO %s (uuid, identity_key, added_timestamp, trust_level)
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();
237 private void deleteIdentity(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
243 ).formatted(TABLE_IDENTITY
);
244 try (final var statement
= connection
.prepareStatement(sql
)) {
245 statement
.setBytes(1, serviceId
.toByteArray());
246 statement
.executeUpdate();
250 private IdentityInfo
getIdentityInfoFromResultSet(ResultSet resultSet
) throws SQLException
{
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");
257 return new IdentityInfo(serviceId
, id
, trustLevel
, added
);
258 } catch (InvalidKeyException e
) {
259 logger
.warn("Failed to load identity key, resetting: {}", e
.getMessage());