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 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();
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 address TEXT 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 return saveIdentity(serviceId
.toString(), identityKey
);
62 boolean saveIdentity(final String address
, final IdentityKey identityKey
) {
63 if (isRetryingDecryption
) {
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
);
74 saveNewIdentity(connection
, address
, identityKey
, identityInfo
== null);
76 } catch (SQLException e
) {
77 throw new RuntimeException("Failed update identity store", e
);
81 public void setRetryingDecryption(final boolean retryingDecryption
) {
82 isRetryingDecryption
= retryingDecryption
;
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
);
93 if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
94 logger
.debug("Not updating trust level for recipient {}, different identity found", serviceId
);
97 if (identityInfo
.getTrustLevel() == trustLevel
) {
98 logger
.trace("Not updating trust level for recipient {}, trust level already matches", serviceId
);
102 logger
.debug("Updating trust level for recipient {} with trust {}", serviceId
, trustLevel
);
103 final var newIdentityInfo
= new IdentityInfo(address
,
106 identityInfo
.getDateAddedTimestamp());
107 storeIdentity(connection
, newIdentityInfo
);
109 } catch (SQLException e
) {
110 throw new RuntimeException("Failed update identity store", e
);
114 public boolean isTrustedIdentity(ServiceId serviceId
, IdentityKey identityKey
, Direction direction
) {
115 return isTrustedIdentity(serviceId
.toString(), identityKey
, direction
);
118 public boolean isTrustedIdentity(String address
, IdentityKey identityKey
, Direction direction
) {
119 if (trustNewIdentity
== TrustNewIdentity
.ALWAYS
) {
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
);
137 logger
.trace("Trusting identity for {} for {}: {}", address
, direction
, false);
142 final var isTrusted
= identityInfo
!= null && identityInfo
.isTrusted();
143 logger
.trace("Trusting identity for {} for {}: {}", address
, direction
, isTrusted
);
145 } catch (SQLException e
) {
146 throw new RuntimeException("Failed read from identity store", e
);
150 public IdentityInfo
getIdentityInfo(ServiceId serviceId
) {
151 return getIdentityInfo(serviceId
.toString());
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
);
162 public List
<IdentityInfo
> getIdentities() {
163 try (final var connection
= database
.getConnection()) {
166 SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level
169 ).formatted(TABLE_IDENTITY
);
170 try (final var statement
= connection
.prepareStatement(sql
)) {
171 return Utils
.executeQueryForStream(statement
, this::getIdentityInfoFromResultSet
)
172 .filter(Objects
::nonNull
)
175 } catch (SQLException e
) {
176 throw new RuntimeException("Failed read from identity store", e
);
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
);
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
);
197 } catch (SQLException e
) {
198 throw new RuntimeException("Failed update identity store", e
);
200 logger
.debug("Complete identities migration took {}ms", (System
.nanoTime() - start
) / 1000000);
203 private IdentityInfo
loadIdentity(
204 final Connection connection
, final String address
205 ) throws SQLException
{
208 SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level
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);
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
);
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());
244 INSERT OR REPLACE INTO %s (address, identity_key, added_timestamp, trust_level)
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();
257 private void deleteIdentity(final Connection connection
, final String address
) throws SQLException
{
263 ).formatted(TABLE_IDENTITY
);
264 try (final var statement
= connection
.prepareStatement(sql
)) {
265 statement
.setString(1, address
);
266 statement
.executeUpdate();
270 private IdentityInfo
getIdentityInfoFromResultSet(ResultSet resultSet
) throws SQLException
{
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");
277 return new IdentityInfo(address
, id
, trustLevel
, added
);
278 } catch (InvalidKeyException e
) {
279 logger
.warn("Failed to load identity key, resetting: {}", e
.getMessage());