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
.asamk
.signal
.manager
.util
.Utils
;
10 import org
.slf4j
.Logger
;
11 import org
.slf4j
.LoggerFactory
;
12 import org
.whispersystems
.libsignal
.IdentityKey
;
13 import org
.whispersystems
.libsignal
.IdentityKeyPair
;
14 import org
.whispersystems
.libsignal
.InvalidKeyException
;
15 import org
.whispersystems
.libsignal
.SignalProtocolAddress
;
17 import java
.io
.ByteArrayInputStream
;
18 import java
.io
.ByteArrayOutputStream
;
20 import java
.io
.FileInputStream
;
21 import java
.io
.FileOutputStream
;
22 import java
.io
.IOException
;
23 import java
.nio
.file
.Files
;
24 import java
.util
.Arrays
;
25 import java
.util
.Base64
;
26 import java
.util
.Date
;
27 import java
.util
.HashMap
;
28 import java
.util
.List
;
30 import java
.util
.regex
.Pattern
;
31 import java
.util
.stream
.Collectors
;
33 public class IdentityKeyStore
implements org
.whispersystems
.libsignal
.state
.IdentityKeyStore
{
35 private final static Logger logger
= LoggerFactory
.getLogger(IdentityKeyStore
.class);
36 private final ObjectMapper objectMapper
= org
.asamk
.signal
.manager
.storage
.Utils
.createStorageObjectMapper();
38 private final Map
<RecipientId
, IdentityInfo
> cachedIdentities
= new HashMap
<>();
40 private final File identitiesPath
;
42 private final RecipientResolver resolver
;
43 private final IdentityKeyPair identityKeyPair
;
44 private final int localRegistrationId
;
46 public IdentityKeyStore(
47 final File identitiesPath
,
48 final RecipientResolver resolver
,
49 final IdentityKeyPair identityKeyPair
,
50 final int localRegistrationId
52 this.identitiesPath
= identitiesPath
;
53 this.resolver
= resolver
;
54 this.identityKeyPair
= identityKeyPair
;
55 this.localRegistrationId
= localRegistrationId
;
59 public IdentityKeyPair
getIdentityKeyPair() {
60 return identityKeyPair
;
64 public int getLocalRegistrationId() {
65 return localRegistrationId
;
69 public boolean saveIdentity(SignalProtocolAddress address
, IdentityKey identityKey
) {
70 final var recipientId
= resolveRecipient(address
.getName());
72 return saveIdentity(recipientId
, identityKey
, new Date());
75 public boolean saveIdentity(final RecipientId recipientId
, final IdentityKey identityKey
, Date added
) {
76 synchronized (cachedIdentities
) {
77 final var identityInfo
= loadIdentityLocked(recipientId
);
78 if (identityInfo
!= null && identityInfo
.getIdentityKey().equals(identityKey
)) {
79 // Identity already exists, not updating the trust level
83 final var trustLevel
= identityInfo
== null ? TrustLevel
.TRUSTED_UNVERIFIED
: TrustLevel
.UNTRUSTED
;
84 final var newIdentityInfo
= new IdentityInfo(recipientId
, identityKey
, trustLevel
, added
);
85 storeIdentityLocked(recipientId
, newIdentityInfo
);
90 public boolean setIdentityTrustLevel(
91 RecipientId recipientId
, IdentityKey identityKey
, TrustLevel trustLevel
93 synchronized (cachedIdentities
) {
94 final var identityInfo
= loadIdentityLocked(recipientId
);
95 if (identityInfo
== null || !identityInfo
.getIdentityKey().equals(identityKey
)) {
96 // Identity not found, not updating the trust level
100 final var newIdentityInfo
= new IdentityInfo(recipientId
,
103 identityInfo
.getDateAdded());
104 storeIdentityLocked(recipientId
, newIdentityInfo
);
110 public boolean isTrustedIdentity(SignalProtocolAddress address
, IdentityKey identityKey
, Direction direction
) {
111 var recipientId
= resolveRecipient(address
.getName());
113 synchronized (cachedIdentities
) {
114 final var identityInfo
= loadIdentityLocked(recipientId
);
115 if (identityInfo
== null) {
116 // Identity not found
120 // TODO implement possibility for different handling of incoming/outgoing trust decisions
121 if (!identityInfo
.getIdentityKey().equals(identityKey
)) {
122 // Identity found, but different
126 return identityInfo
.isTrusted();
131 public IdentityKey
getIdentity(SignalProtocolAddress address
) {
132 var recipientId
= resolveRecipient(address
.getName());
134 synchronized (cachedIdentities
) {
135 var identity
= loadIdentityLocked(recipientId
);
136 return identity
== null ?
null : identity
.getIdentityKey();
140 public IdentityInfo
getIdentity(RecipientId recipientId
) {
141 synchronized (cachedIdentities
) {
142 return loadIdentityLocked(recipientId
);
146 final Pattern identityFileNamePattern
= Pattern
.compile("([0-9]+)");
148 public List
<IdentityInfo
> getIdentities() {
149 final var files
= identitiesPath
.listFiles();
153 return Arrays
.stream(files
)
154 .filter(f
-> identityFileNamePattern
.matcher(f
.getName()).matches())
155 .map(f
-> RecipientId
.of(Integer
.parseInt(f
.getName())))
156 .map(this::loadIdentityLocked
)
157 .collect(Collectors
.toList());
160 public void mergeRecipients(final RecipientId recipientId
, final RecipientId toBeMergedRecipientId
) {
161 synchronized (cachedIdentities
) {
162 deleteIdentityLocked(toBeMergedRecipientId
);
167 * @param identifier can be either a serialized uuid or a e164 phone number
169 private RecipientId
resolveRecipient(String identifier
) {
170 return resolver
.resolveRecipient(Utils
.getSignalServiceAddressFromIdentifier(identifier
));
173 private File
getIdentityFile(final RecipientId recipientId
) {
175 IOUtils
.createPrivateDirectories(identitiesPath
);
176 } catch (IOException e
) {
177 throw new AssertionError("Failed to create identities path", e
);
179 return new File(identitiesPath
, String
.valueOf(recipientId
.getId()));
182 private IdentityInfo
loadIdentityLocked(final RecipientId recipientId
) {
184 final var session
= cachedIdentities
.get(recipientId
);
185 if (session
!= null) {
190 final var file
= getIdentityFile(recipientId
);
191 if (!file
.exists()) {
194 try (var inputStream
= new FileInputStream(file
)) {
195 var storage
= objectMapper
.readValue(inputStream
, IdentityStorage
.class);
197 var id
= new IdentityKey(Base64
.getDecoder().decode(storage
.getIdentityKey()));
198 var trustLevel
= TrustLevel
.fromInt(storage
.getTrustLevel());
199 var added
= new Date(storage
.getAddedTimestamp());
201 final var identityInfo
= new IdentityInfo(recipientId
, id
, trustLevel
, added
);
202 cachedIdentities
.put(recipientId
, identityInfo
);
204 } catch (IOException
| InvalidKeyException e
) {
205 logger
.warn("Failed to load identity key: {}", e
.getMessage());
210 private void storeIdentityLocked(final RecipientId recipientId
, final IdentityInfo identityInfo
) {
211 cachedIdentities
.put(recipientId
, identityInfo
);
213 var storage
= new IdentityStorage(Base64
.getEncoder().encodeToString(identityInfo
.getIdentityKey().serialize()),
214 identityInfo
.getTrustLevel().ordinal(),
215 identityInfo
.getDateAdded().getTime());
217 final var file
= getIdentityFile(recipientId
);
218 // Write to memory first to prevent corrupting the file in case of serialization errors
219 try (var inMemoryOutput
= new ByteArrayOutputStream()) {
220 objectMapper
.writeValue(inMemoryOutput
, storage
);
222 var input
= new ByteArrayInputStream(inMemoryOutput
.toByteArray());
223 try (var outputStream
= new FileOutputStream(file
)) {
224 input
.transferTo(outputStream
);
226 } catch (Exception e
) {
227 logger
.error("Error saving identity file: {}", e
.getMessage());
231 private void deleteIdentityLocked(final RecipientId recipientId
) {
232 cachedIdentities
.remove(recipientId
);
234 final var file
= getIdentityFile(recipientId
);
235 if (!file
.exists()) {
239 Files
.delete(file
.toPath());
240 } catch (IOException e
) {
241 logger
.error("Failed to delete identity file {}: {}", file
, e
.getMessage());
245 private static final class IdentityStorage
{
247 private String identityKey
;
248 private int trustLevel
;
249 private long addedTimestamp
;
251 // For deserialization
252 private IdentityStorage() {
255 private IdentityStorage(final String identityKey
, final int trustLevel
, final long addedTimestamp
) {
256 this.identityKey
= identityKey
;
257 this.trustLevel
= trustLevel
;
258 this.addedTimestamp
= addedTimestamp
;
261 public String
getIdentityKey() {
265 public int getTrustLevel() {
269 public long getAddedTimestamp() {
270 return addedTimestamp
;