1 package org
.asamk
.signal
.manager
.storage
.identities
;
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
;
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
;
21 import io
.reactivex
.rxjava3
.core
.Observable
;
22 import io
.reactivex
.rxjava3
.subjects
.PublishSubject
;
24 public class IdentityKeyStore
{
26 private final static 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();
32 private boolean isRetryingDecryption
= false;
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 uuid BLOB UNIQUE NOT NULL,
41 identity_key BLOB NOT NULL,
42 added_timestamp INTEGER NOT NULL,
43 trust_level INTEGER NOT NULL
49 public IdentityKeyStore(final Database database
, final TrustNewIdentity trustNewIdentity
) {
50 this.database
= database
;
51 this.trustNewIdentity
= trustNewIdentity
;
54 public Observable
<ServiceId
> getIdentityChanges() {
55 return identityChanges
;
58 public boolean saveIdentity(final ServiceId serviceId
, final IdentityKey identityKey
) {
59 if (isRetryingDecryption
) {
62 try (final var connection
= database
.getConnection()) {
63 final var identityInfo
= loadIdentity(connection
, serviceId
);
64 if (identityInfo
!= null && identityInfo
.getIdentityKey().equals(identityKey
)) {
65 // Identity already exists, not updating the trust level
66 logger
.trace("Not storing new identity for recipient {}, identity already stored", serviceId
);
70 saveNewIdentity(connection
, serviceId
, identityKey
, identityInfo
== null);
72 } catch (SQLException e
) {
73 throw new RuntimeException("Failed update identity store", e
);
77 public void setRetryingDecryption(final boolean retryingDecryption
) {
78 isRetryingDecryption
= retryingDecryption
;
81 public boolean setIdentityTrustLevel(ServiceId serviceId
, IdentityKey identityKey
, TrustLevel trustLevel
) {
82 try (final var connection
= database
.getConnection()) {
83 final var identityInfo
= loadIdentity(connection
, serviceId
);
84 if (identityInfo
== null) {
85 logger
.debug("Not updating trust level for recipient {}, identity not found", serviceId
);
88 if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
89 logger
.debug("Not updating trust level for recipient {}, different identity found", serviceId
);
92 if (identityInfo
.getTrustLevel() == trustLevel
) {
93 logger
.trace("Not updating trust level for recipient {}, trust level already matches", serviceId
);
97 logger
.debug("Updating trust level for recipient {} with trust {}", serviceId
, trustLevel
);
98 final var newIdentityInfo
= new IdentityInfo(serviceId
,
101 identityInfo
.getDateAddedTimestamp());
102 storeIdentity(connection
, newIdentityInfo
);
104 } catch (SQLException e
) {
105 throw new RuntimeException("Failed update identity store", e
);
109 public boolean isTrustedIdentity(ServiceId serviceId
, IdentityKey identityKey
, Direction direction
) {
110 if (trustNewIdentity
== TrustNewIdentity
.ALWAYS
) {
114 try (final var connection
= database
.getConnection()) {
115 // TODO implement possibility for different handling of incoming/outgoing trust decisions
116 var identityInfo
= loadIdentity(connection
, serviceId
);
117 if (identityInfo
== null) {
118 logger
.debug("Initial identity found for {}, saving.", serviceId
);
119 saveNewIdentity(connection
, serviceId
, identityKey
, true);
120 identityInfo
= loadIdentity(connection
, serviceId
);
121 } else if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
122 // Identity found, but different
123 if (direction
== Direction
.SENDING
) {
124 logger
.debug("Changed identity found for {}, saving.", serviceId
);
125 saveNewIdentity(connection
, serviceId
, identityKey
, false);
126 identityInfo
= loadIdentity(connection
, serviceId
);
128 logger
.trace("Trusting identity for {} for {}: {}", serviceId
, direction
, false);
133 final var isTrusted
= identityInfo
!= null && identityInfo
.isTrusted();
134 logger
.trace("Trusting identity for {} for {}: {}", serviceId
, direction
, isTrusted
);
136 } catch (SQLException e
) {
137 throw new RuntimeException("Failed read from identity store", e
);
141 public IdentityInfo
getIdentityInfo(ServiceId serviceId
) {
142 try (final var connection
= database
.getConnection()) {
143 return loadIdentity(connection
, serviceId
);
144 } catch (SQLException e
) {
145 throw new RuntimeException("Failed read from identity store", e
);
149 public List
<IdentityInfo
> getIdentities() {
150 try (final var connection
= database
.getConnection()) {
153 SELECT i.uuid, i.identity_key, i.added_timestamp, i.trust_level
156 ).formatted(TABLE_IDENTITY
);
157 try (final var statement
= connection
.prepareStatement(sql
)) {
158 return Utils
.executeQueryForStream(statement
, this::getIdentityInfoFromResultSet
)
159 .filter(Objects
::nonNull
)
162 } catch (SQLException e
) {
163 throw new RuntimeException("Failed read from identity store", e
);
167 public void deleteIdentity(final ServiceId serviceId
) {
168 try (final var connection
= database
.getConnection()) {
169 deleteIdentity(connection
, serviceId
);
170 } catch (SQLException e
) {
171 throw new RuntimeException("Failed update identity store", e
);
175 void addLegacyIdentities(final Collection
<IdentityInfo
> identities
) {
176 logger
.debug("Migrating legacy identities to database");
177 long start
= System
.nanoTime();
178 try (final var connection
= database
.getConnection()) {
179 connection
.setAutoCommit(false);
180 for (final var identityInfo
: identities
) {
181 storeIdentity(connection
, identityInfo
);
184 } catch (SQLException e
) {
185 throw new RuntimeException("Failed update identity store", e
);
187 logger
.debug("Complete identities migration took {}ms", (System
.nanoTime() - start
) / 1000000);
190 private IdentityInfo
loadIdentity(
191 final Connection connection
, final ServiceId serviceId
192 ) throws SQLException
{
195 SELECT i.uuid, i.identity_key, i.added_timestamp, i.trust_level
199 ).formatted(TABLE_IDENTITY
);
200 try (final var statement
= connection
.prepareStatement(sql
)) {
201 statement
.setBytes(1, serviceId
.toByteArray());
202 return Utils
.executeQueryForOptional(statement
, this::getIdentityInfoFromResultSet
).orElse(null);
206 private void saveNewIdentity(
207 final Connection connection
,
208 final ServiceId serviceId
,
209 final IdentityKey identityKey
,
210 final boolean firstIdentity
211 ) throws SQLException
{
212 final var trustLevel
= trustNewIdentity
== TrustNewIdentity
.ALWAYS
|| (
213 trustNewIdentity
== TrustNewIdentity
.ON_FIRST_USE
&& firstIdentity
214 ) ? TrustLevel
.TRUSTED_UNVERIFIED
: TrustLevel
.UNTRUSTED
;
215 logger
.debug("Storing new identity for recipient {} with trust {}", serviceId
, trustLevel
);
216 final var newIdentityInfo
= new IdentityInfo(serviceId
, identityKey
, trustLevel
, System
.currentTimeMillis());
217 storeIdentity(connection
, newIdentityInfo
);
218 identityChanges
.onNext(serviceId
);
221 private void storeIdentity(final Connection connection
, final IdentityInfo identityInfo
) throws SQLException
{
222 logger
.trace("Storing identity info for {}, trust: {}, added: {}",
223 identityInfo
.getServiceId(),
224 identityInfo
.getTrustLevel(),
225 identityInfo
.getDateAddedTimestamp());
228 INSERT OR REPLACE INTO %s (uuid, identity_key, added_timestamp, trust_level)
231 ).formatted(TABLE_IDENTITY
);
232 try (final var statement
= connection
.prepareStatement(sql
)) {
233 statement
.setBytes(1, identityInfo
.getServiceId().toByteArray());
234 statement
.setBytes(2, identityInfo
.getIdentityKey().serialize());
235 statement
.setLong(3, identityInfo
.getDateAddedTimestamp());
236 statement
.setInt(4, identityInfo
.getTrustLevel().ordinal());
237 statement
.executeUpdate();
241 private void deleteIdentity(final Connection connection
, final ServiceId serviceId
) throws SQLException
{
247 ).formatted(TABLE_IDENTITY
);
248 try (final var statement
= connection
.prepareStatement(sql
)) {
249 statement
.setBytes(1, serviceId
.toByteArray());
250 statement
.executeUpdate();
254 private IdentityInfo
getIdentityInfoFromResultSet(ResultSet resultSet
) throws SQLException
{
256 final var serviceId
= ServiceId
.parseOrThrow(resultSet
.getBytes("uuid"));
257 final var id
= new IdentityKey(resultSet
.getBytes("identity_key"));
258 final var trustLevel
= TrustLevel
.fromInt(resultSet
.getInt("trust_level"));
259 final var added
= resultSet
.getLong("added_timestamp");
261 return new IdentityInfo(serviceId
, id
, trustLevel
, added
);
262 } catch (InvalidKeyException e
) {
263 logger
.warn("Failed to load identity key, resetting: {}", e
.getMessage());