1 package org
.asamk
.signal
.manager
.storage
.identities
;
3 import com
.fasterxml
.jackson
.databind
.ObjectMapper
;
5 import org
.asamk
.signal
.manager
.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
.slf4j
.Logger
;
10 import org
.slf4j
.LoggerFactory
;
11 import org
.whispersystems
.libsignal
.IdentityKey
;
12 import org
.whispersystems
.libsignal
.IdentityKeyPair
;
13 import org
.whispersystems
.libsignal
.InvalidKeyException
;
14 import org
.whispersystems
.libsignal
.SignalProtocolAddress
;
16 import java
.io
.ByteArrayInputStream
;
17 import java
.io
.ByteArrayOutputStream
;
19 import java
.io
.FileInputStream
;
20 import java
.io
.FileOutputStream
;
21 import java
.io
.IOException
;
22 import java
.nio
.file
.Files
;
23 import java
.util
.Arrays
;
24 import java
.util
.Base64
;
25 import java
.util
.Date
;
26 import java
.util
.HashMap
;
27 import java
.util
.List
;
29 import java
.util
.Objects
;
30 import java
.util
.regex
.Pattern
;
32 public class IdentityKeyStore
implements org
.whispersystems
.libsignal
.state
.IdentityKeyStore
{
34 private final static Logger logger
= LoggerFactory
.getLogger(IdentityKeyStore
.class);
35 private final ObjectMapper objectMapper
= org
.asamk
.signal
.manager
.storage
.Utils
.createStorageObjectMapper();
37 private final Map
<RecipientId
, IdentityInfo
> cachedIdentities
= new HashMap
<>();
39 private final File identitiesPath
;
41 private final RecipientResolver resolver
;
42 private final IdentityKeyPair identityKeyPair
;
43 private final int localRegistrationId
;
44 private final TrustNewIdentity trustNewIdentity
;
46 private boolean isRetryingDecryption
= false;
48 public IdentityKeyStore(
49 final File identitiesPath
,
50 final RecipientResolver resolver
,
51 final IdentityKeyPair identityKeyPair
,
52 final int localRegistrationId
,
53 final TrustNewIdentity trustNewIdentity
55 this.identitiesPath
= identitiesPath
;
56 this.resolver
= resolver
;
57 this.identityKeyPair
= identityKeyPair
;
58 this.localRegistrationId
= localRegistrationId
;
59 this.trustNewIdentity
= trustNewIdentity
;
63 public IdentityKeyPair
getIdentityKeyPair() {
64 return identityKeyPair
;
68 public int getLocalRegistrationId() {
69 return localRegistrationId
;
73 public boolean saveIdentity(SignalProtocolAddress address
, IdentityKey identityKey
) {
74 final var recipientId
= resolveRecipient(address
.getName());
76 return saveIdentity(recipientId
, identityKey
, new Date());
79 public boolean saveIdentity(final RecipientId recipientId
, final IdentityKey identityKey
, Date added
) {
80 if (isRetryingDecryption
) {
83 synchronized (cachedIdentities
) {
84 final var identityInfo
= loadIdentityLocked(recipientId
);
85 if (identityInfo
!= null && identityInfo
.getIdentityKey().equals(identityKey
)) {
86 // Identity already exists, not updating the trust level
90 final var trustLevel
= trustNewIdentity
== TrustNewIdentity
.ALWAYS
|| (
91 trustNewIdentity
== TrustNewIdentity
.ON_FIRST_USE
&& identityInfo
== null
92 ) ? TrustLevel
.TRUSTED_UNVERIFIED
: TrustLevel
.UNTRUSTED
;
93 logger
.debug("Storing new identity for recipient {} with trust {}", recipientId
, trustLevel
);
94 final var newIdentityInfo
= new IdentityInfo(recipientId
, identityKey
, trustLevel
, added
);
95 storeIdentityLocked(recipientId
, newIdentityInfo
);
100 public void setRetryingDecryption(final boolean retryingDecryption
) {
101 isRetryingDecryption
= retryingDecryption
;
104 public boolean setIdentityTrustLevel(
105 RecipientId recipientId
, IdentityKey identityKey
, TrustLevel trustLevel
107 synchronized (cachedIdentities
) {
108 final var identityInfo
= loadIdentityLocked(recipientId
);
109 if (identityInfo
== null
110 || !identityInfo
.getIdentityKey().equals(identityKey
)
111 || identityInfo
.getTrustLevel() == trustLevel
) {
112 // Identity not found or trust not changed, not updating the trust level
116 final var newIdentityInfo
= new IdentityInfo(recipientId
,
119 identityInfo
.getDateAdded());
120 storeIdentityLocked(recipientId
, newIdentityInfo
);
126 public boolean isTrustedIdentity(SignalProtocolAddress address
, IdentityKey identityKey
, Direction direction
) {
127 if (trustNewIdentity
== TrustNewIdentity
.ALWAYS
) {
131 var recipientId
= resolveRecipient(address
.getName());
133 synchronized (cachedIdentities
) {
134 // TODO implement possibility for different handling of incoming/outgoing trust decisions
135 var identityInfo
= loadIdentityLocked(recipientId
);
136 if (identityInfo
== null) {
137 // Identity not found
138 saveIdentity(address
, identityKey
);
139 return trustNewIdentity
== TrustNewIdentity
.ON_FIRST_USE
;
142 if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
143 // Identity found, but different
144 if (direction
== Direction
.SENDING
) {
145 saveIdentity(address
, identityKey
);
146 identityInfo
= loadIdentityLocked(recipientId
);
150 return identityInfo
.isTrusted();
155 public IdentityKey
getIdentity(SignalProtocolAddress address
) {
156 var recipientId
= resolveRecipient(address
.getName());
158 synchronized (cachedIdentities
) {
159 var identity
= loadIdentityLocked(recipientId
);
160 return identity
== null ?
null : identity
.getIdentityKey();
164 public IdentityInfo
getIdentity(RecipientId recipientId
) {
165 synchronized (cachedIdentities
) {
166 return loadIdentityLocked(recipientId
);
170 final Pattern identityFileNamePattern
= Pattern
.compile("([0-9]+)");
172 public List
<IdentityInfo
> getIdentities() {
173 final var files
= identitiesPath
.listFiles();
177 return Arrays
.stream(files
)
178 .filter(f
-> identityFileNamePattern
.matcher(f
.getName()).matches())
179 .map(f
-> resolver
.resolveRecipient(Long
.parseLong(f
.getName())))
180 .filter(Objects
::nonNull
)
181 .map(this::loadIdentityLocked
)
185 public void mergeRecipients(final RecipientId recipientId
, final RecipientId toBeMergedRecipientId
) {
186 synchronized (cachedIdentities
) {
187 deleteIdentityLocked(toBeMergedRecipientId
);
191 public void deleteIdentity(final RecipientId recipientId
) {
192 synchronized (cachedIdentities
) {
193 deleteIdentityLocked(recipientId
);
198 * @param identifier can be either a serialized uuid or a e164 phone number
200 private RecipientId
resolveRecipient(String identifier
) {
201 return resolver
.resolveRecipient(identifier
);
204 private File
getIdentityFile(final RecipientId recipientId
) {
206 IOUtils
.createPrivateDirectories(identitiesPath
);
207 } catch (IOException e
) {
208 throw new AssertionError("Failed to create identities path", e
);
210 return new File(identitiesPath
, String
.valueOf(recipientId
.id()));
213 private IdentityInfo
loadIdentityLocked(final RecipientId recipientId
) {
215 final var session
= cachedIdentities
.get(recipientId
);
216 if (session
!= null) {
221 final var file
= getIdentityFile(recipientId
);
222 if (!file
.exists()) {
225 try (var inputStream
= new FileInputStream(file
)) {
226 var storage
= objectMapper
.readValue(inputStream
, IdentityStorage
.class);
228 var id
= new IdentityKey(Base64
.getDecoder().decode(storage
.identityKey()));
229 var trustLevel
= TrustLevel
.fromInt(storage
.trustLevel());
230 var added
= new Date(storage
.addedTimestamp());
232 final var identityInfo
= new IdentityInfo(recipientId
, id
, trustLevel
, added
);
233 cachedIdentities
.put(recipientId
, identityInfo
);
235 } catch (IOException
| InvalidKeyException e
) {
236 logger
.warn("Failed to load identity key: {}", e
.getMessage());
241 private void storeIdentityLocked(final RecipientId recipientId
, final IdentityInfo identityInfo
) {
242 cachedIdentities
.put(recipientId
, identityInfo
);
244 var storage
= new IdentityStorage(Base64
.getEncoder().encodeToString(identityInfo
.getIdentityKey().serialize()),
245 identityInfo
.getTrustLevel().ordinal(),
246 identityInfo
.getDateAdded().getTime());
248 final var file
= getIdentityFile(recipientId
);
249 // Write to memory first to prevent corrupting the file in case of serialization errors
250 try (var inMemoryOutput
= new ByteArrayOutputStream()) {
251 objectMapper
.writeValue(inMemoryOutput
, storage
);
253 var input
= new ByteArrayInputStream(inMemoryOutput
.toByteArray());
254 try (var outputStream
= new FileOutputStream(file
)) {
255 input
.transferTo(outputStream
);
257 } catch (Exception e
) {
258 logger
.error("Error saving identity file: {}", e
.getMessage());
262 private void deleteIdentityLocked(final RecipientId recipientId
) {
263 cachedIdentities
.remove(recipientId
);
265 final var file
= getIdentityFile(recipientId
);
266 if (!file
.exists()) {
270 Files
.delete(file
.toPath());
271 } catch (IOException e
) {
272 logger
.error("Failed to delete identity file {}: {}", file
, e
.getMessage());
276 private record IdentityStorage(String identityKey
, int trustLevel
, long addedTimestamp
) {}