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
.asamk
.signal
.manager
.storage
.recipients
.RecipientStore
;
8 import org
.signal
.libsignal
.protocol
.IdentityKey
;
9 import org
.signal
.libsignal
.protocol
.InvalidKeyException
;
10 import org
.signal
.libsignal
.protocol
.state
.IdentityKeyStore
.Direction
;
11 import org
.slf4j
.Logger
;
12 import org
.slf4j
.LoggerFactory
;
13 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
15 import java
.sql
.Connection
;
16 import java
.sql
.ResultSet
;
17 import java
.sql
.SQLException
;
18 import java
.util
.Collection
;
19 import java
.util
.List
;
20 import java
.util
.Objects
;
22 import io
.reactivex
.rxjava3
.core
.Observable
;
23 import io
.reactivex
.rxjava3
.subjects
.PublishSubject
;
25 public class IdentityKeyStore
{
27 private static final Logger logger
= LoggerFactory
.getLogger(IdentityKeyStore
.class);
28 private static final String TABLE_IDENTITY
= "identity";
29 private final Database database
;
30 private final TrustNewIdentity trustNewIdentity
;
31 private final RecipientStore recipientStore
;
32 private final PublishSubject
<ServiceId
> identityChanges
= PublishSubject
.create();
34 private boolean isRetryingDecryption
= false;
36 public static void createSql(Connection connection
) throws SQLException
{
37 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
38 try (final var statement
= connection
.createStatement()) {
39 statement
.executeUpdate("""
40 CREATE TABLE identity (
41 _id INTEGER PRIMARY KEY,
42 address TEXT UNIQUE NOT NULL,
43 identity_key BLOB NOT NULL,
44 added_timestamp INTEGER NOT NULL,
45 trust_level INTEGER NOT NULL
51 public IdentityKeyStore(
52 final Database database
, final TrustNewIdentity trustNewIdentity
, RecipientStore recipientStore
54 this.database
= database
;
55 this.trustNewIdentity
= trustNewIdentity
;
56 this.recipientStore
= recipientStore
;
59 public Observable
<ServiceId
> getIdentityChanges() {
60 return identityChanges
;
63 public boolean saveIdentity(final ServiceId serviceId
, final IdentityKey identityKey
) {
64 return saveIdentity(serviceId
.toString(), identityKey
);
67 public boolean saveIdentity(
68 final Connection connection
, final ServiceId serviceId
, final IdentityKey identityKey
69 ) throws SQLException
{
70 return saveIdentity(connection
, serviceId
.toString(), identityKey
);
73 boolean saveIdentity(final String address
, final IdentityKey identityKey
) {
74 if (isRetryingDecryption
) {
77 try (final var connection
= database
.getConnection()) {
78 return saveIdentity(connection
, address
, identityKey
);
79 } catch (SQLException e
) {
80 throw new RuntimeException("Failed update identity store", e
);
84 private boolean saveIdentity(
85 final Connection connection
, final String address
, final IdentityKey identityKey
86 ) throws SQLException
{
87 final var identityInfo
= loadIdentity(connection
, address
);
88 if (identityInfo
!= null && identityInfo
.getIdentityKey().equals(identityKey
)) {
89 // Identity already exists, not updating the trust level
90 logger
.trace("Not storing new identity for recipient {}, identity already stored", address
);
94 saveNewIdentity(connection
, address
, identityKey
, identityInfo
== null);
98 public void setRetryingDecryption(final boolean retryingDecryption
) {
99 isRetryingDecryption
= retryingDecryption
;
102 public boolean setIdentityTrustLevel(ServiceId serviceId
, IdentityKey identityKey
, TrustLevel trustLevel
) {
103 try (final var connection
= database
.getConnection()) {
104 return setIdentityTrustLevel(connection
, serviceId
, identityKey
, trustLevel
);
105 } catch (SQLException e
) {
106 throw new RuntimeException("Failed update identity store", e
);
110 public boolean setIdentityTrustLevel(
111 final Connection connection
,
112 final ServiceId serviceId
,
113 final IdentityKey identityKey
,
114 final TrustLevel trustLevel
115 ) throws SQLException
{
116 final var address
= serviceId
.toString();
117 final var identityInfo
= loadIdentity(connection
, address
);
118 if (identityInfo
== null) {
119 logger
.debug("Not updating trust level for recipient {}, identity not found", serviceId
);
122 if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
123 logger
.debug("Not updating trust level for recipient {}, different identity found", serviceId
);
126 if (identityInfo
.getTrustLevel() == trustLevel
) {
127 logger
.trace("Not updating trust level for recipient {}, trust level already matches", serviceId
);
131 logger
.debug("Updating trust level for recipient {} with trust {}", serviceId
, trustLevel
);
132 final var newIdentityInfo
= new IdentityInfo(address
,
135 identityInfo
.getDateAddedTimestamp());
136 storeIdentity(connection
, newIdentityInfo
);
140 public boolean isTrustedIdentity(ServiceId serviceId
, IdentityKey identityKey
, Direction direction
) {
141 return isTrustedIdentity(serviceId
.toString(), identityKey
, direction
);
144 public boolean isTrustedIdentity(String address
, IdentityKey identityKey
, Direction direction
) {
145 if (trustNewIdentity
== TrustNewIdentity
.ALWAYS
) {
149 try (final var connection
= database
.getConnection()) {
150 // TODO implement possibility for different handling of incoming/outgoing trust decisions
151 var identityInfo
= loadIdentity(connection
, address
);
152 if (identityInfo
== null) {
153 logger
.debug("Initial identity found for {}, saving.", address
);
154 saveNewIdentity(connection
, address
, identityKey
, true);
155 identityInfo
= loadIdentity(connection
, address
);
156 } else if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
157 // Identity found, but different
158 if (direction
== Direction
.SENDING
) {
159 logger
.debug("Changed identity found for {}, saving.", address
);
160 saveNewIdentity(connection
, address
, identityKey
, false);
161 identityInfo
= loadIdentity(connection
, address
);
163 logger
.trace("Trusting identity for {} for {}: {}", address
, direction
, false);
168 final var isTrusted
= identityInfo
!= null && identityInfo
.isTrusted();
169 logger
.trace("Trusting identity for {} for {}: {}", address
, direction
, isTrusted
);
171 } catch (SQLException e
) {
172 throw new RuntimeException("Failed read from identity store", e
);
176 public IdentityInfo
getIdentityInfo(ServiceId serviceId
) {
177 return getIdentityInfo(serviceId
.toString());
180 public IdentityInfo
getIdentityInfo(String address
) {
181 try (final var connection
= database
.getConnection()) {
182 return loadIdentity(connection
, address
);
183 } catch (SQLException e
) {
184 throw new RuntimeException("Failed read from identity store", e
);
188 public IdentityInfo
getIdentityInfo(Connection connection
, String address
) throws SQLException
{
189 return loadIdentity(connection
, address
);
192 public List
<IdentityInfo
> getIdentities() {
193 try (final var connection
= database
.getConnection()) {
196 SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level
199 ).formatted(TABLE_IDENTITY
);
200 try (final var statement
= connection
.prepareStatement(sql
)) {
201 return Utils
.executeQueryForStream(statement
, this::getIdentityInfoFromResultSet
)
202 .filter(Objects
::nonNull
)
205 } catch (SQLException e
) {
206 throw new RuntimeException("Failed read from identity store", e
);
210 public void deleteIdentity(final ServiceId serviceId
) {
211 try (final var connection
= database
.getConnection()) {
212 deleteIdentity(connection
, serviceId
.toString());
213 } catch (SQLException e
) {
214 throw new RuntimeException("Failed update identity store", e
);
218 void addLegacyIdentities(final Collection
<IdentityInfo
> identities
) {
219 logger
.debug("Migrating legacy identities to database");
220 long start
= System
.nanoTime();
221 try (final var connection
= database
.getConnection()) {
222 connection
.setAutoCommit(false);
223 for (final var identityInfo
: identities
) {
224 storeIdentity(connection
, identityInfo
);
227 } catch (SQLException e
) {
228 throw new RuntimeException("Failed update identity store", e
);
230 logger
.debug("Complete identities migration took {}ms", (System
.nanoTime() - start
) / 1000000);
233 private IdentityInfo
loadIdentity(
234 final Connection connection
, final String address
235 ) throws SQLException
{
238 SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level
242 ).formatted(TABLE_IDENTITY
);
243 try (final var statement
= connection
.prepareStatement(sql
)) {
244 statement
.setString(1, address
);
245 return Utils
.executeQueryForOptional(statement
, this::getIdentityInfoFromResultSet
).orElse(null);
249 private void saveNewIdentity(
250 final Connection connection
,
251 final String address
,
252 final IdentityKey identityKey
,
253 final boolean firstIdentity
254 ) throws SQLException
{
255 final var trustLevel
= trustNewIdentity
== TrustNewIdentity
.ALWAYS
|| (
256 trustNewIdentity
== TrustNewIdentity
.ON_FIRST_USE
&& firstIdentity
257 ) ? TrustLevel
.TRUSTED_UNVERIFIED
: TrustLevel
.UNTRUSTED
;
258 logger
.debug("Storing new identity for recipient {} with trust {}", address
, trustLevel
);
259 final var newIdentityInfo
= new IdentityInfo(address
, identityKey
, trustLevel
, System
.currentTimeMillis());
260 storeIdentity(connection
, newIdentityInfo
);
261 final var serviceId
= ServiceId
.parseOrNull(address
);
262 if (serviceId
!= null) {
263 identityChanges
.onNext(serviceId
);
267 private void storeIdentity(final Connection connection
, final IdentityInfo identityInfo
) throws SQLException
{
268 logger
.trace("Storing identity info for {}, trust: {}, added: {}",
269 identityInfo
.getServiceId(),
270 identityInfo
.getTrustLevel(),
271 identityInfo
.getDateAddedTimestamp());
274 INSERT OR REPLACE INTO %s (address, identity_key, added_timestamp, trust_level)
277 ).formatted(TABLE_IDENTITY
);
278 try (final var statement
= connection
.prepareStatement(sql
)) {
279 statement
.setString(1, identityInfo
.getAddress());
280 statement
.setBytes(2, identityInfo
.getIdentityKey().serialize());
281 statement
.setLong(3, identityInfo
.getDateAddedTimestamp());
282 statement
.setInt(4, identityInfo
.getTrustLevel().ordinal());
283 statement
.executeUpdate();
285 recipientStore
.rotateStorageId(connection
, identityInfo
.getServiceId());
288 private void deleteIdentity(final Connection connection
, final String address
) throws SQLException
{
294 ).formatted(TABLE_IDENTITY
);
295 try (final var statement
= connection
.prepareStatement(sql
)) {
296 statement
.setString(1, address
);
297 statement
.executeUpdate();
301 private IdentityInfo
getIdentityInfoFromResultSet(ResultSet resultSet
) throws SQLException
{
303 final var address
= resultSet
.getString("address");
304 final var id
= new IdentityKey(resultSet
.getBytes("identity_key"));
305 final var trustLevel
= TrustLevel
.fromInt(resultSet
.getInt("trust_level"));
306 final var added
= resultSet
.getLong("added_timestamp");
308 return new IdentityInfo(address
, id
, trustLevel
, added
);
309 } catch (InvalidKeyException e
) {
310 logger
.warn("Failed to load identity key, resetting: {}", e
.getMessage());