1 package org
.asamk
.signal
.manager
.storage
.identities
;
3 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
5 import org
.asamk
.signal
.manager
.api
.TrustLevel
;
6 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientId
;
7 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientResolver
;
8 import org
.asamk
.signal
.manager
.util
.IOUtils
;
9 import org
.signal
.libsignal
.protocol
.IdentityKey
;
10 import org
.signal
.libsignal
.protocol
.InvalidKeyException
;
11 import org
.signal
.libsignal
.protocol
.state
.IdentityKeyStore
.Direction
;
12 import org
.slf4j
.Logger
;
13 import org
.slf4j
.LoggerFactory
;
15 import java
.io
.ByteArrayInputStream
;
16 import java
.io
.ByteArrayOutputStream
;
18 import java
.io
.FileInputStream
;
19 import java
.io
.FileOutputStream
;
20 import java
.io
.IOException
;
21 import java
.nio
.file
.Files
;
22 import java
.util
.Arrays
;
23 import java
.util
.Base64
;
24 import java
.util
.Date
;
25 import java
.util
.HashMap
;
26 import java
.util
.List
;
28 import java
.util
.Objects
;
29 import java
.util
.regex
.Pattern
;
31 import io
.reactivex
.rxjava3
.subjects
.PublishSubject
;
32 import io
.reactivex
.rxjava3
.subjects
.Subject
;
34 public class IdentityKeyStore
{
36 private final static Logger logger
= LoggerFactory
.getLogger(IdentityKeyStore
.class);
37 private final ObjectMapper objectMapper
= org
.asamk
.signal
.manager
.storage
.Utils
.createStorageObjectMapper();
39 private final Map
<RecipientId
, IdentityInfo
> cachedIdentities
= new HashMap
<>();
41 private final File identitiesPath
;
43 private final RecipientResolver resolver
;
44 private final TrustNewIdentity trustNewIdentity
;
45 private final PublishSubject
<RecipientId
> identityChanges
= PublishSubject
.create();
47 private boolean isRetryingDecryption
= false;
49 public IdentityKeyStore(
50 final File identitiesPath
, final RecipientResolver resolver
, final TrustNewIdentity trustNewIdentity
52 this.identitiesPath
= identitiesPath
;
53 this.resolver
= resolver
;
54 this.trustNewIdentity
= trustNewIdentity
;
57 public Subject
<RecipientId
> getIdentityChanges() {
58 return identityChanges
;
61 public boolean saveIdentity(final RecipientId recipientId
, final IdentityKey identityKey
) {
62 return saveIdentity(recipientId
, identityKey
, null);
65 public boolean saveIdentity(final RecipientId recipientId
, final IdentityKey identityKey
, Date added
) {
66 if (isRetryingDecryption
) {
69 synchronized (cachedIdentities
) {
70 final var identityInfo
= loadIdentityLocked(recipientId
);
71 if (identityInfo
!= null && identityInfo
.getIdentityKey().equals(identityKey
)) {
72 // Identity already exists, not updating the trust level
73 logger
.trace("Not storing new identity for recipient {}, identity already stored", recipientId
);
77 final var trustLevel
= trustNewIdentity
== TrustNewIdentity
.ALWAYS
|| (
78 trustNewIdentity
== TrustNewIdentity
.ON_FIRST_USE
&& identityInfo
== null
79 ) ? TrustLevel
.TRUSTED_UNVERIFIED
: TrustLevel
.UNTRUSTED
;
80 logger
.debug("Storing new identity for recipient {} with trust {}", recipientId
, trustLevel
);
81 final var newIdentityInfo
= new IdentityInfo(recipientId
,
84 added
== null ?
new Date() : added
);
85 storeIdentityLocked(recipientId
, newIdentityInfo
);
86 identityChanges
.onNext(recipientId
);
91 public void setRetryingDecryption(final boolean retryingDecryption
) {
92 isRetryingDecryption
= retryingDecryption
;
95 public boolean setIdentityTrustLevel(RecipientId recipientId
, IdentityKey identityKey
, TrustLevel trustLevel
) {
96 synchronized (cachedIdentities
) {
97 final var identityInfo
= loadIdentityLocked(recipientId
);
98 if (identityInfo
== null) {
99 logger
.debug("Not updating trust level for recipient {}, identity not found", recipientId
);
102 if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
103 logger
.debug("Not updating trust level for recipient {}, different identity found", recipientId
);
106 if (identityInfo
.getTrustLevel() == trustLevel
) {
107 logger
.trace("Not updating trust level for recipient {}, trust level already matches", recipientId
);
111 logger
.debug("Updating trust level for recipient {} with trust {}", recipientId
, trustLevel
);
112 final var newIdentityInfo
= new IdentityInfo(recipientId
,
115 identityInfo
.getDateAdded());
116 storeIdentityLocked(recipientId
, newIdentityInfo
);
121 public boolean isTrustedIdentity(RecipientId recipientId
, IdentityKey identityKey
, Direction direction
) {
122 if (trustNewIdentity
== TrustNewIdentity
.ALWAYS
) {
126 synchronized (cachedIdentities
) {
127 // TODO implement possibility for different handling of incoming/outgoing trust decisions
128 var identityInfo
= loadIdentityLocked(recipientId
);
129 if (identityInfo
== null) {
130 logger
.debug("Initial identity found for {}, saving.", recipientId
);
131 saveIdentity(recipientId
, identityKey
);
132 identityInfo
= loadIdentityLocked(recipientId
);
133 } else if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
134 // Identity found, but different
135 if (direction
== Direction
.SENDING
) {
136 logger
.debug("Changed identity found for {}, saving.", recipientId
);
137 saveIdentity(recipientId
, identityKey
);
138 identityInfo
= loadIdentityLocked(recipientId
);
140 logger
.trace("Trusting identity for {} for {}: {}", recipientId
, direction
, false);
145 final var isTrusted
= identityInfo
!= null && identityInfo
.isTrusted();
146 logger
.trace("Trusting identity for {} for {}: {}", recipientId
, direction
, isTrusted
);
151 public IdentityKey
getIdentity(RecipientId recipientId
) {
152 synchronized (cachedIdentities
) {
153 var identity
= loadIdentityLocked(recipientId
);
154 return identity
== null ?
null : identity
.getIdentityKey();
158 public IdentityInfo
getIdentityInfo(RecipientId recipientId
) {
159 synchronized (cachedIdentities
) {
160 return loadIdentityLocked(recipientId
);
164 final Pattern identityFileNamePattern
= Pattern
.compile("(\\d+)");
166 public List
<IdentityInfo
> getIdentities() {
167 final var files
= identitiesPath
.listFiles();
171 return Arrays
.stream(files
)
172 .filter(f
-> identityFileNamePattern
.matcher(f
.getName()).matches())
173 .map(f
-> resolver
.resolveRecipient(Long
.parseLong(f
.getName())))
174 .filter(Objects
::nonNull
)
175 .map(this::loadIdentityLocked
)
176 .filter(Objects
::nonNull
)
180 public void mergeRecipients(final RecipientId recipientId
, final RecipientId toBeMergedRecipientId
) {
181 synchronized (cachedIdentities
) {
182 deleteIdentityLocked(toBeMergedRecipientId
);
186 public void deleteIdentity(final RecipientId recipientId
) {
187 synchronized (cachedIdentities
) {
188 deleteIdentityLocked(recipientId
);
192 private File
getIdentityFile(final RecipientId recipientId
) {
194 IOUtils
.createPrivateDirectories(identitiesPath
);
195 } catch (IOException e
) {
196 throw new AssertionError("Failed to create identities path", e
);
198 return new File(identitiesPath
, String
.valueOf(recipientId
.id()));
201 private IdentityInfo
loadIdentityLocked(final RecipientId recipientId
) {
203 final var session
= cachedIdentities
.get(recipientId
);
204 if (session
!= null) {
209 final var file
= getIdentityFile(recipientId
);
210 if (!file
.exists()) {
213 try (var inputStream
= new FileInputStream(file
)) {
214 var storage
= objectMapper
.readValue(inputStream
, IdentityStorage
.class);
216 var id
= new IdentityKey(Base64
.getDecoder().decode(storage
.identityKey()));
217 var trustLevel
= TrustLevel
.fromInt(storage
.trustLevel());
218 var added
= new Date(storage
.addedTimestamp());
220 final var identityInfo
= new IdentityInfo(recipientId
, id
, trustLevel
, added
);
221 cachedIdentities
.put(recipientId
, identityInfo
);
223 } catch (IOException
| InvalidKeyException e
) {
224 logger
.warn("Failed to load identity key: {}", e
.getMessage());
229 private void storeIdentityLocked(final RecipientId recipientId
, final IdentityInfo identityInfo
) {
230 logger
.trace("Storing identity info for {}, trust: {}, added: {}",
232 identityInfo
.getTrustLevel(),
233 identityInfo
.getDateAdded());
234 cachedIdentities
.put(recipientId
, identityInfo
);
236 var storage
= new IdentityStorage(Base64
.getEncoder().encodeToString(identityInfo
.getIdentityKey().serialize()),
237 identityInfo
.getTrustLevel().ordinal(),
238 identityInfo
.getDateAdded().getTime());
240 final var file
= getIdentityFile(recipientId
);
241 // Write to memory first to prevent corrupting the file in case of serialization errors
242 try (var inMemoryOutput
= new ByteArrayOutputStream()) {
243 objectMapper
.writeValue(inMemoryOutput
, storage
);
245 var input
= new ByteArrayInputStream(inMemoryOutput
.toByteArray());
246 try (var outputStream
= new FileOutputStream(file
)) {
247 input
.transferTo(outputStream
);
249 } catch (Exception e
) {
250 logger
.error("Error saving identity file: {}", e
.getMessage());
254 private void deleteIdentityLocked(final RecipientId recipientId
) {
255 cachedIdentities
.remove(recipientId
);
257 final var file
= getIdentityFile(recipientId
);
258 if (!file
.exists()) {
262 Files
.delete(file
.toPath());
263 } catch (IOException e
) {
264 logger
.error("Failed to delete identity file {}: {}", file
, e
.getMessage());
268 private record IdentityStorage(String identityKey
, int trustLevel
, long addedTimestamp
) {}