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
,
53 final TrustNewIdentity trustNewIdentity
,
54 RecipientStore recipientStore
56 this.database
= database
;
57 this.trustNewIdentity
= trustNewIdentity
;
58 this.recipientStore
= recipientStore
;
61 public Observable
<ServiceId
> getIdentityChanges() {
62 return identityChanges
;
65 public boolean saveIdentity(final ServiceId serviceId
, final IdentityKey identityKey
) {
66 return saveIdentity(serviceId
.toString(), identityKey
);
69 public boolean saveIdentity(
70 final Connection connection
,
71 final ServiceId serviceId
,
72 final IdentityKey identityKey
73 ) throws SQLException
{
74 return saveIdentity(connection
, serviceId
.toString(), identityKey
);
77 boolean saveIdentity(final String address
, final IdentityKey identityKey
) {
78 if (isRetryingDecryption
) {
81 try (final var connection
= database
.getConnection()) {
82 return saveIdentity(connection
, address
, identityKey
);
83 } catch (SQLException e
) {
84 throw new RuntimeException("Failed update identity store", e
);
88 private boolean saveIdentity(
89 final Connection connection
,
91 final IdentityKey identityKey
92 ) throws SQLException
{
93 final var identityInfo
= loadIdentity(connection
, address
);
94 if (identityInfo
!= null && identityInfo
.getIdentityKey().equals(identityKey
)) {
95 // Identity already exists, not updating the trust level
96 logger
.trace("Not storing new identity for recipient {}, identity already stored", address
);
100 saveNewIdentity(connection
, address
, identityKey
, identityInfo
== null);
104 public void setRetryingDecryption(final boolean retryingDecryption
) {
105 isRetryingDecryption
= retryingDecryption
;
108 public boolean setIdentityTrustLevel(ServiceId serviceId
, IdentityKey identityKey
, TrustLevel trustLevel
) {
109 try (final var connection
= database
.getConnection()) {
110 return setIdentityTrustLevel(connection
, serviceId
, identityKey
, trustLevel
);
111 } catch (SQLException e
) {
112 throw new RuntimeException("Failed update identity store", e
);
116 public boolean setIdentityTrustLevel(
117 final Connection connection
,
118 final ServiceId serviceId
,
119 final IdentityKey identityKey
,
120 final TrustLevel trustLevel
121 ) throws SQLException
{
122 final var address
= serviceId
.toString();
123 final var identityInfo
= loadIdentity(connection
, address
);
124 if (identityInfo
== null) {
125 logger
.debug("Not updating trust level for recipient {}, identity not found", serviceId
);
128 if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
129 logger
.debug("Not updating trust level for recipient {}, different identity found", serviceId
);
132 if (identityInfo
.getTrustLevel() == trustLevel
) {
133 logger
.trace("Not updating trust level for recipient {}, trust level already matches", serviceId
);
137 logger
.debug("Updating trust level for recipient {} with trust {}", serviceId
, trustLevel
);
138 final var newIdentityInfo
= new IdentityInfo(address
,
141 identityInfo
.getDateAddedTimestamp());
142 storeIdentity(connection
, newIdentityInfo
);
146 public boolean isTrustedIdentity(ServiceId serviceId
, IdentityKey identityKey
, Direction direction
) {
147 return isTrustedIdentity(serviceId
.toString(), identityKey
, direction
);
150 public boolean isTrustedIdentity(String address
, IdentityKey identityKey
, Direction direction
) {
151 if (trustNewIdentity
== TrustNewIdentity
.ALWAYS
) {
155 try (final var connection
= database
.getConnection()) {
156 // TODO implement possibility for different handling of incoming/outgoing trust decisions
157 var identityInfo
= loadIdentity(connection
, address
);
158 if (identityInfo
== null) {
159 logger
.debug("Initial identity found for {}, saving.", address
);
160 saveNewIdentity(connection
, address
, identityKey
, true);
161 identityInfo
= loadIdentity(connection
, address
);
162 } else if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
163 // Identity found, but different
164 if (direction
== Direction
.SENDING
) {
165 logger
.debug("Changed identity found for {}, saving.", address
);
166 saveNewIdentity(connection
, address
, identityKey
, false);
167 identityInfo
= loadIdentity(connection
, address
);
169 logger
.trace("Trusting identity for {} for {}: {}", address
, direction
, false);
174 final var isTrusted
= identityInfo
!= null && identityInfo
.isTrusted();
175 logger
.trace("Trusting identity for {} for {}: {}", address
, direction
, isTrusted
);
177 } catch (SQLException e
) {
178 throw new RuntimeException("Failed read from identity store", e
);
182 public IdentityInfo
getIdentityInfo(ServiceId serviceId
) {
183 return getIdentityInfo(serviceId
.toString());
186 public IdentityInfo
getIdentityInfo(String address
) {
187 try (final var connection
= database
.getConnection()) {
188 return loadIdentity(connection
, address
);
189 } catch (SQLException e
) {
190 throw new RuntimeException("Failed read from identity store", e
);
194 public IdentityInfo
getIdentityInfo(Connection connection
, String address
) throws SQLException
{
195 return loadIdentity(connection
, address
);
198 public List
<IdentityInfo
> getIdentities() {
199 try (final var connection
= database
.getConnection()) {
202 SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level
205 ).formatted(TABLE_IDENTITY
);
206 try (final var statement
= connection
.prepareStatement(sql
)) {
207 return Utils
.executeQueryForStream(statement
, this::getIdentityInfoFromResultSet
)
208 .filter(Objects
::nonNull
)
211 } catch (SQLException e
) {
212 throw new RuntimeException("Failed read from identity store", e
);
216 public void deleteIdentity(final ServiceId serviceId
) {
217 try (final var connection
= database
.getConnection()) {
218 deleteIdentity(connection
, serviceId
.toString());
219 } catch (SQLException e
) {
220 throw new RuntimeException("Failed update identity store", e
);
224 void addLegacyIdentities(final Collection
<IdentityInfo
> identities
) {
225 logger
.debug("Migrating legacy identities to database");
226 long start
= System
.nanoTime();
227 try (final var connection
= database
.getConnection()) {
228 connection
.setAutoCommit(false);
229 for (final var identityInfo
: identities
) {
230 storeIdentity(connection
, identityInfo
);
233 } catch (SQLException e
) {
234 throw new RuntimeException("Failed update identity store", e
);
236 logger
.debug("Complete identities migration took {}ms", (System
.nanoTime() - start
) / 1000000);
239 private IdentityInfo
loadIdentity(final Connection connection
, final String address
) throws SQLException
{
242 SELECT i.address, i.identity_key, i.added_timestamp, i.trust_level
246 ).formatted(TABLE_IDENTITY
);
247 try (final var statement
= connection
.prepareStatement(sql
)) {
248 statement
.setString(1, address
);
249 return Utils
.executeQueryForOptional(statement
, this::getIdentityInfoFromResultSet
).orElse(null);
253 private void saveNewIdentity(
254 final Connection connection
,
255 final String address
,
256 final IdentityKey identityKey
,
257 final boolean firstIdentity
258 ) throws SQLException
{
259 final var trustLevel
= trustNewIdentity
== TrustNewIdentity
.ALWAYS
|| (
260 trustNewIdentity
== TrustNewIdentity
.ON_FIRST_USE
&& firstIdentity
261 ) ? TrustLevel
.TRUSTED_UNVERIFIED
: TrustLevel
.UNTRUSTED
;
262 logger
.debug("Storing new identity for recipient {} with trust {}", address
, trustLevel
);
263 final var newIdentityInfo
= new IdentityInfo(address
, identityKey
, trustLevel
, System
.currentTimeMillis());
264 storeIdentity(connection
, newIdentityInfo
);
265 final var serviceId
= ServiceId
.parseOrNull(address
);
266 if (serviceId
!= null) {
267 identityChanges
.onNext(serviceId
);
271 private void storeIdentity(final Connection connection
, final IdentityInfo identityInfo
) throws SQLException
{
272 logger
.trace("Storing identity info for {}, trust: {}, added: {}",
273 identityInfo
.getServiceId(),
274 identityInfo
.getTrustLevel(),
275 identityInfo
.getDateAddedTimestamp());
278 INSERT OR REPLACE INTO %s (address, identity_key, added_timestamp, trust_level)
281 ).formatted(TABLE_IDENTITY
);
282 try (final var statement
= connection
.prepareStatement(sql
)) {
283 statement
.setString(1, identityInfo
.getAddress());
284 statement
.setBytes(2, identityInfo
.getIdentityKey().serialize());
285 statement
.setLong(3, identityInfo
.getDateAddedTimestamp());
286 statement
.setInt(4, identityInfo
.getTrustLevel().ordinal());
287 statement
.executeUpdate();
289 recipientStore
.rotateStorageId(connection
, identityInfo
.getServiceId());
292 private void deleteIdentity(final Connection connection
, final String address
) throws SQLException
{
298 ).formatted(TABLE_IDENTITY
);
299 try (final var statement
= connection
.prepareStatement(sql
)) {
300 statement
.setString(1, address
);
301 statement
.executeUpdate();
305 private IdentityInfo
getIdentityInfoFromResultSet(ResultSet resultSet
) throws SQLException
{
307 final var address
= resultSet
.getString("address");
308 final var id
= new IdentityKey(resultSet
.getBytes("identity_key"));
309 final var trustLevel
= TrustLevel
.fromInt(resultSet
.getInt("trust_level"));
310 final var added
= resultSet
.getLong("added_timestamp");
312 return new IdentityInfo(address
, id
, trustLevel
, added
);
313 } catch (InvalidKeyException e
) {
314 logger
.warn("Failed to load identity key, resetting: {}", e
.getMessage());