]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
67fc252958208b1a9a5558a7beb156347b592b8a
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / SignalAccount.java
1 package org.asamk.signal.manager.storage;
2
3 import com.fasterxml.jackson.databind.JsonNode;
4 import com.fasterxml.jackson.databind.ObjectMapper;
5
6 import org.asamk.signal.manager.TrustLevel;
7 import org.asamk.signal.manager.api.Pair;
8 import org.asamk.signal.manager.groups.GroupId;
9 import org.asamk.signal.manager.storage.configuration.ConfigurationStore;
10 import org.asamk.signal.manager.storage.contacts.ContactsStore;
11 import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
12 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
13 import org.asamk.signal.manager.storage.groups.GroupInfoV2;
14 import org.asamk.signal.manager.storage.groups.GroupStore;
15 import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
16 import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
17 import org.asamk.signal.manager.storage.messageCache.MessageCache;
18 import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
19 import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore;
20 import org.asamk.signal.manager.storage.profiles.LegacyProfileStore;
21 import org.asamk.signal.manager.storage.profiles.ProfileStore;
22 import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore;
23 import org.asamk.signal.manager.storage.protocol.SignalProtocolStore;
24 import org.asamk.signal.manager.storage.recipients.Contact;
25 import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
26 import org.asamk.signal.manager.storage.recipients.Profile;
27 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
28 import org.asamk.signal.manager.storage.recipients.RecipientId;
29 import org.asamk.signal.manager.storage.recipients.RecipientStore;
30 import org.asamk.signal.manager.storage.senderKeys.SenderKeyStore;
31 import org.asamk.signal.manager.storage.sessions.SessionStore;
32 import org.asamk.signal.manager.storage.stickers.StickerStore;
33 import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
34 import org.asamk.signal.manager.util.IOUtils;
35 import org.asamk.signal.manager.util.KeyUtils;
36 import org.signal.zkgroup.InvalidInputException;
37 import org.signal.zkgroup.profiles.ProfileKey;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40 import org.whispersystems.libsignal.IdentityKeyPair;
41 import org.whispersystems.libsignal.SignalProtocolAddress;
42 import org.whispersystems.libsignal.state.PreKeyRecord;
43 import org.whispersystems.libsignal.state.SessionRecord;
44 import org.whispersystems.libsignal.state.SignedPreKeyRecord;
45 import org.whispersystems.libsignal.util.Medium;
46 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
47 import org.whispersystems.signalservice.api.kbs.MasterKey;
48 import org.whispersystems.signalservice.api.push.ACI;
49 import org.whispersystems.signalservice.api.push.DistributionId;
50 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
51 import org.whispersystems.signalservice.api.storage.StorageKey;
52 import org.whispersystems.signalservice.api.util.UuidUtil;
53
54 import java.io.ByteArrayInputStream;
55 import java.io.ByteArrayOutputStream;
56 import java.io.Closeable;
57 import java.io.File;
58 import java.io.IOException;
59 import java.io.RandomAccessFile;
60 import java.nio.channels.Channels;
61 import java.nio.channels.ClosedChannelException;
62 import java.nio.channels.FileChannel;
63 import java.nio.channels.FileLock;
64 import java.security.SecureRandom;
65 import java.util.Base64;
66 import java.util.Date;
67 import java.util.HashSet;
68 import java.util.List;
69
70 public class SignalAccount implements Closeable {
71
72 private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
73
74 private static final int MINIMUM_STORAGE_VERSION = 1;
75 private static final int CURRENT_STORAGE_VERSION = 3;
76
77 private int previousStorageVersion;
78
79 private final ObjectMapper jsonProcessor = Utils.createStorageObjectMapper();
80
81 private final FileChannel fileChannel;
82 private final FileLock lock;
83
84 private String account;
85 private ACI aci;
86 private String encryptedDeviceName;
87 private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
88 private boolean isMultiDevice = false;
89 private String password;
90 private String registrationLockPin;
91 private MasterKey pinMasterKey;
92 private StorageKey storageKey;
93 private long storageManifestVersion = -1;
94 private ProfileKey profileKey;
95 private int preKeyIdOffset;
96 private int nextSignedPreKeyId;
97 private long lastReceiveTimestamp = 0;
98
99 private boolean registered = false;
100
101 private SignalProtocolStore signalProtocolStore;
102 private PreKeyStore preKeyStore;
103 private SignedPreKeyStore signedPreKeyStore;
104 private SessionStore sessionStore;
105 private IdentityKeyStore identityKeyStore;
106 private SenderKeyStore senderKeyStore;
107 private GroupStore groupStore;
108 private GroupStore.Storage groupStoreStorage;
109 private RecipientStore recipientStore;
110 private StickerStore stickerStore;
111 private StickerStore.Storage stickerStoreStorage;
112 private ConfigurationStore configurationStore;
113 private ConfigurationStore.Storage configurationStoreStorage;
114
115 private MessageCache messageCache;
116
117 private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
118 this.fileChannel = fileChannel;
119 this.lock = lock;
120 }
121
122 public static SignalAccount load(
123 File dataPath, String account, boolean waitForLock, final TrustNewIdentity trustNewIdentity
124 ) throws IOException {
125 final var fileName = getFileName(dataPath, account);
126 final var pair = openFileChannel(fileName, waitForLock);
127 try {
128 var signalAccount = new SignalAccount(pair.first(), pair.second());
129 signalAccount.load(dataPath, trustNewIdentity);
130 signalAccount.migrateLegacyConfigs();
131
132 if (!account.equals(signalAccount.getAccount())) {
133 throw new IOException("Number in account file doesn't match expected number: "
134 + signalAccount.getAccount());
135 }
136
137 return signalAccount;
138 } catch (Throwable e) {
139 pair.second().close();
140 pair.first().close();
141 throw e;
142 }
143 }
144
145 public static SignalAccount create(
146 File dataPath,
147 String account,
148 IdentityKeyPair identityKey,
149 int registrationId,
150 ProfileKey profileKey,
151 final TrustNewIdentity trustNewIdentity
152 ) throws IOException {
153 IOUtils.createPrivateDirectories(dataPath);
154 var fileName = getFileName(dataPath, account);
155 if (!fileName.exists()) {
156 IOUtils.createPrivateFile(fileName);
157 }
158
159 final var pair = openFileChannel(fileName, true);
160 var signalAccount = new SignalAccount(pair.first(), pair.second());
161
162 signalAccount.account = account;
163 signalAccount.profileKey = profileKey;
164
165 signalAccount.initStores(dataPath, identityKey, registrationId, trustNewIdentity);
166 signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, account),
167 signalAccount.recipientStore,
168 signalAccount::saveGroupStore);
169 signalAccount.stickerStore = new StickerStore(signalAccount::saveStickerStore);
170 signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
171
172 signalAccount.registered = false;
173
174 signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
175 signalAccount.migrateLegacyConfigs();
176 signalAccount.save();
177
178 return signalAccount;
179 }
180
181 private void initStores(
182 final File dataPath,
183 final IdentityKeyPair identityKey,
184 final int registrationId,
185 final TrustNewIdentity trustNewIdentity
186 ) throws IOException {
187 recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, account), this::mergeRecipients);
188
189 preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, account));
190 signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, account));
191 sessionStore = new SessionStore(getSessionsPath(dataPath, account), recipientStore);
192 identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, account),
193 recipientStore,
194 identityKey,
195 registrationId,
196 trustNewIdentity);
197 senderKeyStore = new SenderKeyStore(getSharedSenderKeysFile(dataPath, account),
198 getSenderKeysPath(dataPath, account),
199 recipientStore::resolveRecipientAddress,
200 recipientStore);
201 signalProtocolStore = new SignalProtocolStore(preKeyStore,
202 signedPreKeyStore,
203 sessionStore,
204 identityKeyStore,
205 senderKeyStore,
206 this::isMultiDevice);
207
208 messageCache = new MessageCache(getMessageCachePath(dataPath, account));
209 }
210
211 public static SignalAccount createOrUpdateLinkedAccount(
212 File dataPath,
213 String account,
214 ACI aci,
215 String password,
216 String encryptedDeviceName,
217 int deviceId,
218 IdentityKeyPair identityKey,
219 int registrationId,
220 ProfileKey profileKey,
221 final TrustNewIdentity trustNewIdentity
222 ) throws IOException {
223 IOUtils.createPrivateDirectories(dataPath);
224 var fileName = getFileName(dataPath, account);
225 if (!fileName.exists()) {
226 return createLinkedAccount(dataPath,
227 account,
228 aci,
229 password,
230 encryptedDeviceName,
231 deviceId,
232 identityKey,
233 registrationId,
234 profileKey,
235 trustNewIdentity);
236 }
237
238 final var signalAccount = load(dataPath, account, true, trustNewIdentity);
239 signalAccount.setProvisioningData(account, aci, password, encryptedDeviceName, deviceId, profileKey);
240 signalAccount.recipientStore.resolveRecipientTrusted(signalAccount.getSelfAddress());
241 signalAccount.sessionStore.archiveAllSessions();
242 signalAccount.senderKeyStore.deleteAll();
243 signalAccount.clearAllPreKeys();
244 return signalAccount;
245 }
246
247 private void clearAllPreKeys() {
248 this.preKeyIdOffset = new SecureRandom().nextInt(Medium.MAX_VALUE);
249 this.nextSignedPreKeyId = new SecureRandom().nextInt(Medium.MAX_VALUE);
250 this.preKeyStore.removeAllPreKeys();
251 this.signedPreKeyStore.removeAllSignedPreKeys();
252 save();
253 }
254
255 private static SignalAccount createLinkedAccount(
256 File dataPath,
257 String account,
258 ACI aci,
259 String password,
260 String encryptedDeviceName,
261 int deviceId,
262 IdentityKeyPair identityKey,
263 int registrationId,
264 ProfileKey profileKey,
265 final TrustNewIdentity trustNewIdentity
266 ) throws IOException {
267 var fileName = getFileName(dataPath, account);
268 IOUtils.createPrivateFile(fileName);
269
270 final var pair = openFileChannel(fileName, true);
271 var signalAccount = new SignalAccount(pair.first(), pair.second());
272
273 signalAccount.setProvisioningData(account, aci, password, encryptedDeviceName, deviceId, profileKey);
274
275 signalAccount.initStores(dataPath, identityKey, registrationId, trustNewIdentity);
276 signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, account),
277 signalAccount.recipientStore,
278 signalAccount::saveGroupStore);
279 signalAccount.stickerStore = new StickerStore(signalAccount::saveStickerStore);
280 signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
281
282 signalAccount.recipientStore.resolveRecipientTrusted(signalAccount.getSelfAddress());
283 signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
284 signalAccount.migrateLegacyConfigs();
285 signalAccount.save();
286
287 return signalAccount;
288 }
289
290 private void setProvisioningData(
291 final String account,
292 final ACI aci,
293 final String password,
294 final String encryptedDeviceName,
295 final int deviceId,
296 final ProfileKey profileKey
297 ) {
298 this.account = account;
299 this.aci = aci;
300 this.password = password;
301 this.profileKey = profileKey;
302 this.encryptedDeviceName = encryptedDeviceName;
303 this.deviceId = deviceId;
304 this.registered = true;
305 this.isMultiDevice = true;
306 this.lastReceiveTimestamp = 0;
307 this.pinMasterKey = null;
308 this.storageManifestVersion = -1;
309 this.storageKey = null;
310 }
311
312 private void migrateLegacyConfigs() {
313 if (getPassword() == null) {
314 setPassword(KeyUtils.createPassword());
315 }
316
317 if (getProfileKey() == null) {
318 // Old config file, creating new profile key
319 setProfileKey(KeyUtils.createProfileKey());
320 }
321 // Ensure our profile key is stored in profile store
322 getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey());
323 if (previousStorageVersion < 3) {
324 for (final var group : groupStore.getGroups()) {
325 if (group instanceof GroupInfoV2 && group.getDistributionId() == null) {
326 ((GroupInfoV2) group).setDistributionId(DistributionId.create());
327 groupStore.updateGroup(group);
328 }
329 }
330 save();
331 }
332 }
333
334 private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
335 sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId);
336 identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
337 messageCache.mergeRecipients(recipientId, toBeMergedRecipientId);
338 groupStore.mergeRecipients(recipientId, toBeMergedRecipientId);
339 senderKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
340 }
341
342 public void removeRecipient(final RecipientId recipientId) {
343 sessionStore.deleteAllSessions(recipientId);
344 identityKeyStore.deleteIdentity(recipientId);
345 messageCache.deleteMessages(recipientId);
346 senderKeyStore.deleteAll(recipientId);
347 recipientStore.deleteRecipientData(recipientId);
348 }
349
350 public static File getFileName(File dataPath, String account) {
351 return new File(dataPath, account);
352 }
353
354 private static File getUserPath(final File dataPath, final String account) {
355 final var path = new File(dataPath, account + ".d");
356 try {
357 IOUtils.createPrivateDirectories(path);
358 } catch (IOException e) {
359 throw new AssertionError("Failed to create user path", e);
360 }
361 return path;
362 }
363
364 private static File getMessageCachePath(File dataPath, String account) {
365 return new File(getUserPath(dataPath, account), "msg-cache");
366 }
367
368 private static File getGroupCachePath(File dataPath, String account) {
369 return new File(getUserPath(dataPath, account), "group-cache");
370 }
371
372 private static File getPreKeysPath(File dataPath, String account) {
373 return new File(getUserPath(dataPath, account), "pre-keys");
374 }
375
376 private static File getSignedPreKeysPath(File dataPath, String account) {
377 return new File(getUserPath(dataPath, account), "signed-pre-keys");
378 }
379
380 private static File getIdentitiesPath(File dataPath, String account) {
381 return new File(getUserPath(dataPath, account), "identities");
382 }
383
384 private static File getSessionsPath(File dataPath, String account) {
385 return new File(getUserPath(dataPath, account), "sessions");
386 }
387
388 private static File getSenderKeysPath(File dataPath, String account) {
389 return new File(getUserPath(dataPath, account), "sender-keys");
390 }
391
392 private static File getSharedSenderKeysFile(File dataPath, String account) {
393 return new File(getUserPath(dataPath, account), "shared-sender-keys-store");
394 }
395
396 private static File getRecipientsStoreFile(File dataPath, String account) {
397 return new File(getUserPath(dataPath, account), "recipients-store");
398 }
399
400 public static boolean userExists(File dataPath, String account) {
401 if (account == null) {
402 return false;
403 }
404 var f = getFileName(dataPath, account);
405 return !(!f.exists() || f.isDirectory());
406 }
407
408 private void load(
409 File dataPath, final TrustNewIdentity trustNewIdentity
410 ) throws IOException {
411 JsonNode rootNode;
412 synchronized (fileChannel) {
413 fileChannel.position(0);
414 rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
415 }
416
417 if (rootNode.hasNonNull("version")) {
418 var accountVersion = rootNode.get("version").asInt(1);
419 if (accountVersion > CURRENT_STORAGE_VERSION) {
420 throw new IOException("Config file was created by a more recent version!");
421 } else if (accountVersion < MINIMUM_STORAGE_VERSION) {
422 throw new IOException("Config file was created by a no longer supported older version!");
423 }
424 previousStorageVersion = accountVersion;
425 }
426
427 account = Utils.getNotNullNode(rootNode, "username").asText();
428 if (rootNode.hasNonNull("password")) {
429 password = rootNode.get("password").asText();
430 }
431 registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
432 if (rootNode.hasNonNull("uuid")) {
433 try {
434 aci = ACI.parseOrThrow(rootNode.get("uuid").asText());
435 } catch (IllegalArgumentException e) {
436 throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e);
437 }
438 }
439 if (rootNode.hasNonNull("deviceName")) {
440 encryptedDeviceName = rootNode.get("deviceName").asText();
441 }
442 if (rootNode.hasNonNull("deviceId")) {
443 deviceId = rootNode.get("deviceId").asInt();
444 }
445 if (rootNode.hasNonNull("isMultiDevice")) {
446 isMultiDevice = rootNode.get("isMultiDevice").asBoolean();
447 }
448 if (rootNode.hasNonNull("lastReceiveTimestamp")) {
449 lastReceiveTimestamp = rootNode.get("lastReceiveTimestamp").asLong();
450 }
451 int registrationId = 0;
452 if (rootNode.hasNonNull("registrationId")) {
453 registrationId = rootNode.get("registrationId").asInt();
454 }
455 IdentityKeyPair identityKeyPair = null;
456 if (rootNode.hasNonNull("identityPrivateKey") && rootNode.hasNonNull("identityKey")) {
457 final var publicKeyBytes = Base64.getDecoder().decode(rootNode.get("identityKey").asText());
458 final var privateKeyBytes = Base64.getDecoder().decode(rootNode.get("identityPrivateKey").asText());
459 identityKeyPair = KeyUtils.getIdentityKeyPair(publicKeyBytes, privateKeyBytes);
460 }
461
462 if (rootNode.hasNonNull("registrationLockPin")) {
463 registrationLockPin = rootNode.get("registrationLockPin").asText();
464 }
465 if (rootNode.hasNonNull("pinMasterKey")) {
466 pinMasterKey = new MasterKey(Base64.getDecoder().decode(rootNode.get("pinMasterKey").asText()));
467 }
468 if (rootNode.hasNonNull("storageKey")) {
469 storageKey = new StorageKey(Base64.getDecoder().decode(rootNode.get("storageKey").asText()));
470 }
471 if (rootNode.hasNonNull("storageManifestVersion")) {
472 storageManifestVersion = rootNode.get("storageManifestVersion").asLong();
473 }
474 if (rootNode.hasNonNull("preKeyIdOffset")) {
475 preKeyIdOffset = rootNode.get("preKeyIdOffset").asInt(0);
476 } else {
477 preKeyIdOffset = 0;
478 }
479 if (rootNode.hasNonNull("nextSignedPreKeyId")) {
480 nextSignedPreKeyId = rootNode.get("nextSignedPreKeyId").asInt();
481 } else {
482 nextSignedPreKeyId = 0;
483 }
484 if (rootNode.hasNonNull("profileKey")) {
485 try {
486 profileKey = new ProfileKey(Base64.getDecoder().decode(rootNode.get("profileKey").asText()));
487 } catch (InvalidInputException e) {
488 throw new IOException(
489 "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
490 e);
491 }
492 }
493
494 var migratedLegacyConfig = false;
495 final var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
496 ? jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
497 LegacyJsonSignalProtocolStore.class)
498 : null;
499 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
500 identityKeyPair = legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentityKeyPair();
501 registrationId = legacySignalProtocolStore.getLegacyIdentityKeyStore().getLocalRegistrationId();
502 migratedLegacyConfig = true;
503 }
504
505 initStores(dataPath, identityKeyPair, registrationId, trustNewIdentity);
506
507 migratedLegacyConfig = loadLegacyStores(rootNode, legacySignalProtocolStore) || migratedLegacyConfig;
508
509 if (rootNode.hasNonNull("groupStore")) {
510 groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class);
511 groupStore = GroupStore.fromStorage(groupStoreStorage,
512 getGroupCachePath(dataPath, account),
513 recipientStore,
514 this::saveGroupStore);
515 } else {
516 groupStore = new GroupStore(getGroupCachePath(dataPath, account), recipientStore, this::saveGroupStore);
517 }
518
519 if (rootNode.hasNonNull("stickerStore")) {
520 stickerStoreStorage = jsonProcessor.convertValue(rootNode.get("stickerStore"), StickerStore.Storage.class);
521 stickerStore = StickerStore.fromStorage(stickerStoreStorage, this::saveStickerStore);
522 } else {
523 stickerStore = new StickerStore(this::saveStickerStore);
524 }
525
526 if (rootNode.hasNonNull("configurationStore")) {
527 configurationStoreStorage = jsonProcessor.convertValue(rootNode.get("configurationStore"),
528 ConfigurationStore.Storage.class);
529 configurationStore = ConfigurationStore.fromStorage(configurationStoreStorage,
530 this::saveConfigurationStore);
531 } else {
532 configurationStore = new ConfigurationStore(this::saveConfigurationStore);
533 }
534
535 migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig;
536
537 if (migratedLegacyConfig) {
538 save();
539 }
540 }
541
542 private boolean loadLegacyStores(
543 final JsonNode rootNode, final LegacyJsonSignalProtocolStore legacySignalProtocolStore
544 ) {
545 var migrated = false;
546 var legacyRecipientStoreNode = rootNode.get("recipientStore");
547 if (legacyRecipientStoreNode != null) {
548 logger.debug("Migrating legacy recipient store.");
549 var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
550 if (legacyRecipientStore != null) {
551 recipientStore.resolveRecipientsTrusted(legacyRecipientStore.getAddresses());
552 }
553 getSelfRecipientId();
554 migrated = true;
555 }
556
557 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
558 logger.debug("Migrating legacy pre key store.");
559 for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
560 try {
561 preKeyStore.storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
562 } catch (IOException e) {
563 logger.warn("Failed to migrate pre key, ignoring", e);
564 }
565 }
566 migrated = true;
567 }
568
569 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
570 logger.debug("Migrating legacy signed pre key store.");
571 for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
572 try {
573 signedPreKeyStore.storeSignedPreKey(entry.getKey(), new SignedPreKeyRecord(entry.getValue()));
574 } catch (IOException e) {
575 logger.warn("Failed to migrate signed pre key, ignoring", e);
576 }
577 }
578 migrated = true;
579 }
580
581 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySessionStore() != null) {
582 logger.debug("Migrating legacy session store.");
583 for (var session : legacySignalProtocolStore.getLegacySessionStore().getSessions()) {
584 try {
585 sessionStore.storeSession(new SignalProtocolAddress(session.address.getIdentifier(),
586 session.deviceId), new SessionRecord(session.sessionRecord));
587 } catch (Exception e) {
588 logger.warn("Failed to migrate session, ignoring", e);
589 }
590 }
591 migrated = true;
592 }
593
594 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
595 logger.debug("Migrating legacy identity session store.");
596 for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
597 RecipientId recipientId = recipientStore.resolveRecipientTrusted(identity.getAddress());
598 identityKeyStore.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
599 identityKeyStore.setIdentityTrustLevel(recipientId,
600 identity.getIdentityKey(),
601 identity.getTrustLevel());
602 }
603 migrated = true;
604 }
605
606 if (rootNode.hasNonNull("contactStore")) {
607 logger.debug("Migrating legacy contact store.");
608 final var contactStoreNode = rootNode.get("contactStore");
609 final var contactStore = jsonProcessor.convertValue(contactStoreNode, LegacyJsonContactsStore.class);
610 for (var contact : contactStore.getContacts()) {
611 final var recipientId = recipientStore.resolveRecipientTrusted(contact.getAddress());
612 recipientStore.storeContact(recipientId,
613 new Contact(contact.name,
614 contact.color,
615 contact.messageExpirationTime,
616 contact.blocked,
617 contact.archived));
618
619 // Store profile keys only in profile store
620 var profileKeyString = contact.profileKey;
621 if (profileKeyString != null) {
622 final ProfileKey profileKey;
623 try {
624 profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
625 getProfileStore().storeProfileKey(recipientId, profileKey);
626 } catch (InvalidInputException e) {
627 logger.warn("Failed to parse legacy contact profile key: {}", e.getMessage());
628 }
629 }
630 }
631 migrated = true;
632 }
633
634 if (rootNode.hasNonNull("profileStore")) {
635 logger.debug("Migrating legacy profile store.");
636 var profileStoreNode = rootNode.get("profileStore");
637 final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class);
638 for (var profileEntry : legacyProfileStore.getProfileEntries()) {
639 var recipientId = recipientStore.resolveRecipient(profileEntry.getAddress());
640 recipientStore.storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential());
641 recipientStore.storeProfileKey(recipientId, profileEntry.getProfileKey());
642 final var profile = profileEntry.getProfile();
643 if (profile != null) {
644 final var capabilities = new HashSet<Profile.Capability>();
645 if (profile.getCapabilities() != null) {
646 if (profile.getCapabilities().gv1Migration) {
647 capabilities.add(Profile.Capability.gv1Migration);
648 }
649 if (profile.getCapabilities().gv2) {
650 capabilities.add(Profile.Capability.gv2);
651 }
652 if (profile.getCapabilities().storage) {
653 capabilities.add(Profile.Capability.storage);
654 }
655 }
656 final var newProfile = new Profile(profileEntry.getLastUpdateTimestamp(),
657 profile.getGivenName(),
658 profile.getFamilyName(),
659 profile.getAbout(),
660 profile.getAboutEmoji(),
661 null,
662 profile.isUnrestrictedUnidentifiedAccess()
663 ? Profile.UnidentifiedAccessMode.UNRESTRICTED
664 : profile.getUnidentifiedAccess() != null
665 ? Profile.UnidentifiedAccessMode.ENABLED
666 : Profile.UnidentifiedAccessMode.DISABLED,
667 capabilities);
668 recipientStore.storeProfile(recipientId, newProfile);
669 }
670 }
671 }
672
673 return migrated;
674 }
675
676 private boolean loadLegacyThreadStore(final JsonNode rootNode) {
677 var threadStoreNode = rootNode.get("threadStore");
678 if (threadStoreNode != null && !threadStoreNode.isNull()) {
679 var threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
680 // Migrate thread info to group and contact store
681 for (var thread : threadStore.getThreads()) {
682 if (thread.id == null || thread.id.isEmpty()) {
683 continue;
684 }
685 try {
686 if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
687 final var recipientId = recipientStore.resolveRecipient(thread.id);
688 var contact = recipientStore.getContact(recipientId);
689 if (contact != null) {
690 recipientStore.storeContact(recipientId,
691 Contact.newBuilder(contact)
692 .withMessageExpirationTime(thread.messageExpirationTime)
693 .build());
694 }
695 } else {
696 var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
697 if (groupInfo instanceof GroupInfoV1) {
698 ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
699 groupStore.updateGroup(groupInfo);
700 }
701 }
702 } catch (Exception e) {
703 logger.warn("Failed to read legacy thread info: {}", e.getMessage());
704 }
705 }
706 return true;
707 }
708
709 return false;
710 }
711
712 private void saveStickerStore(StickerStore.Storage storage) {
713 this.stickerStoreStorage = storage;
714 save();
715 }
716
717 private void saveGroupStore(GroupStore.Storage storage) {
718 this.groupStoreStorage = storage;
719 save();
720 }
721
722 private void saveConfigurationStore(ConfigurationStore.Storage storage) {
723 this.configurationStoreStorage = storage;
724 save();
725 }
726
727 private void save() {
728 synchronized (fileChannel) {
729 var rootNode = jsonProcessor.createObjectNode();
730 rootNode.put("version", CURRENT_STORAGE_VERSION)
731 .put("username", account)
732 .put("uuid", aci == null ? null : aci.toString())
733 .put("deviceName", encryptedDeviceName)
734 .put("deviceId", deviceId)
735 .put("isMultiDevice", isMultiDevice)
736 .put("lastReceiveTimestamp", lastReceiveTimestamp)
737 .put("password", password)
738 .put("registrationId", identityKeyStore.getLocalRegistrationId())
739 .put("identityPrivateKey",
740 Base64.getEncoder()
741 .encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
742 .put("identityKey",
743 Base64.getEncoder()
744 .encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
745 .put("registrationLockPin", registrationLockPin)
746 .put("pinMasterKey",
747 pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
748 .put("storageKey",
749 storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
750 .put("storageManifestVersion", storageManifestVersion == -1 ? null : storageManifestVersion)
751 .put("preKeyIdOffset", preKeyIdOffset)
752 .put("nextSignedPreKeyId", nextSignedPreKeyId)
753 .put("profileKey",
754 profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
755 .put("registered", registered)
756 .putPOJO("groupStore", groupStoreStorage)
757 .putPOJO("stickerStore", stickerStoreStorage)
758 .putPOJO("configurationStore", configurationStoreStorage);
759 try {
760 try (var output = new ByteArrayOutputStream()) {
761 // Write to memory first to prevent corrupting the file in case of serialization errors
762 jsonProcessor.writeValue(output, rootNode);
763 var input = new ByteArrayInputStream(output.toByteArray());
764 fileChannel.position(0);
765 input.transferTo(Channels.newOutputStream(fileChannel));
766 fileChannel.truncate(fileChannel.position());
767 fileChannel.force(false);
768 }
769 } catch (Exception e) {
770 logger.error("Error saving file: {}", e.getMessage());
771 }
772 }
773 }
774
775 private static Pair<FileChannel, FileLock> openFileChannel(File fileName, boolean waitForLock) throws IOException {
776 var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
777 var lock = fileChannel.tryLock();
778 if (lock == null) {
779 if (!waitForLock) {
780 logger.debug("Config file is in use by another instance.");
781 throw new IOException("Config file is in use by another instance.");
782 }
783 logger.info("Config file is in use by another instance, waiting…");
784 lock = fileChannel.lock();
785 logger.info("Config file lock acquired.");
786 }
787 return new Pair<>(fileChannel, lock);
788 }
789
790 public void addPreKeys(List<PreKeyRecord> records) {
791 for (var record : records) {
792 if (preKeyIdOffset != record.getId()) {
793 logger.error("Invalid pre key id {}, expected {}", record.getId(), preKeyIdOffset);
794 throw new AssertionError("Invalid pre key id");
795 }
796 preKeyStore.storePreKey(record.getId(), record);
797 preKeyIdOffset = (preKeyIdOffset + 1) % Medium.MAX_VALUE;
798 }
799 save();
800 }
801
802 public void addSignedPreKey(SignedPreKeyRecord record) {
803 if (nextSignedPreKeyId != record.getId()) {
804 logger.error("Invalid signed pre key id {}, expected {}", record.getId(), nextSignedPreKeyId);
805 throw new AssertionError("Invalid signed pre key id");
806 }
807 signalProtocolStore.storeSignedPreKey(record.getId(), record);
808 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
809 save();
810 }
811
812 public SignalProtocolStore getSignalProtocolStore() {
813 return signalProtocolStore;
814 }
815
816 public SessionStore getSessionStore() {
817 return sessionStore;
818 }
819
820 public IdentityKeyStore getIdentityKeyStore() {
821 return identityKeyStore;
822 }
823
824 public GroupStore getGroupStore() {
825 return groupStore;
826 }
827
828 public ContactsStore getContactStore() {
829 return recipientStore;
830 }
831
832 public RecipientStore getRecipientStore() {
833 return recipientStore;
834 }
835
836 public ProfileStore getProfileStore() {
837 return recipientStore;
838 }
839
840 public StickerStore getStickerStore() {
841 return stickerStore;
842 }
843
844 public SenderKeyStore getSenderKeyStore() {
845 return senderKeyStore;
846 }
847
848 public ConfigurationStore getConfigurationStore() {
849 return configurationStore;
850 }
851
852 public MessageCache getMessageCache() {
853 return messageCache;
854 }
855
856 public String getAccount() {
857 return account;
858 }
859
860 public ACI getAci() {
861 return aci;
862 }
863
864 public void setAci(final ACI aci) {
865 this.aci = aci;
866 save();
867 }
868
869 public SignalServiceAddress getSelfAddress() {
870 return new SignalServiceAddress(aci, account);
871 }
872
873 public RecipientId getSelfRecipientId() {
874 return recipientStore.resolveRecipientTrusted(new RecipientAddress(aci == null ? null : aci.uuid(), account));
875 }
876
877 public String getEncryptedDeviceName() {
878 return encryptedDeviceName;
879 }
880
881 public void setEncryptedDeviceName(final String encryptedDeviceName) {
882 this.encryptedDeviceName = encryptedDeviceName;
883 save();
884 }
885
886 public int getDeviceId() {
887 return deviceId;
888 }
889
890 public boolean isMasterDevice() {
891 return deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID;
892 }
893
894 public IdentityKeyPair getIdentityKeyPair() {
895 return signalProtocolStore.getIdentityKeyPair();
896 }
897
898 public int getLocalRegistrationId() {
899 return signalProtocolStore.getLocalRegistrationId();
900 }
901
902 public String getPassword() {
903 return password;
904 }
905
906 private void setPassword(final String password) {
907 this.password = password;
908 save();
909 }
910
911 public void setRegistrationLockPin(final String registrationLockPin, final MasterKey pinMasterKey) {
912 this.registrationLockPin = registrationLockPin;
913 this.pinMasterKey = pinMasterKey;
914 save();
915 }
916
917 public MasterKey getPinMasterKey() {
918 return pinMasterKey;
919 }
920
921 public StorageKey getStorageKey() {
922 if (pinMasterKey != null) {
923 return pinMasterKey.deriveStorageServiceKey();
924 }
925 return storageKey;
926 }
927
928 public void setStorageKey(final StorageKey storageKey) {
929 if (storageKey.equals(this.storageKey)) {
930 return;
931 }
932 this.storageKey = storageKey;
933 save();
934 }
935
936 public long getStorageManifestVersion() {
937 return this.storageManifestVersion;
938 }
939
940 public void setStorageManifestVersion(final long storageManifestVersion) {
941 if (storageManifestVersion == this.storageManifestVersion) {
942 return;
943 }
944 this.storageManifestVersion = storageManifestVersion;
945 save();
946 }
947
948 public ProfileKey getProfileKey() {
949 return profileKey;
950 }
951
952 public void setProfileKey(final ProfileKey profileKey) {
953 if (profileKey.equals(this.profileKey)) {
954 return;
955 }
956 this.profileKey = profileKey;
957 save();
958 }
959
960 public byte[] getSelfUnidentifiedAccessKey() {
961 return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
962 }
963
964 public int getPreKeyIdOffset() {
965 return preKeyIdOffset;
966 }
967
968 public int getNextSignedPreKeyId() {
969 return nextSignedPreKeyId;
970 }
971
972 public boolean isRegistered() {
973 return registered;
974 }
975
976 public void setRegistered(final boolean registered) {
977 this.registered = registered;
978 save();
979 }
980
981 public boolean isMultiDevice() {
982 return isMultiDevice;
983 }
984
985 public void setMultiDevice(final boolean multiDevice) {
986 if (isMultiDevice == multiDevice) {
987 return;
988 }
989 isMultiDevice = multiDevice;
990 save();
991 }
992
993 public long getLastReceiveTimestamp() {
994 return lastReceiveTimestamp;
995 }
996
997 public void setLastReceiveTimestamp(final long lastReceiveTimestamp) {
998 this.lastReceiveTimestamp = lastReceiveTimestamp;
999 save();
1000 }
1001
1002 public boolean isUnrestrictedUnidentifiedAccess() {
1003 // TODO make configurable
1004 return false;
1005 }
1006
1007 public boolean isDiscoverableByPhoneNumber() {
1008 return configurationStore.getPhoneNumberUnlisted() == null || !configurationStore.getPhoneNumberUnlisted();
1009 }
1010
1011 public void finishRegistration(final ACI aci, final MasterKey masterKey, final String pin) {
1012 this.pinMasterKey = masterKey;
1013 this.storageManifestVersion = -1;
1014 this.storageKey = null;
1015 this.encryptedDeviceName = null;
1016 this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
1017 this.isMultiDevice = false;
1018 this.registered = true;
1019 this.aci = aci;
1020 this.registrationLockPin = pin;
1021 this.lastReceiveTimestamp = 0;
1022 save();
1023
1024 clearAllPreKeys();
1025 getSessionStore().archiveAllSessions();
1026 senderKeyStore.deleteAll();
1027 final var recipientId = getRecipientStore().resolveRecipientTrusted(getSelfAddress());
1028 final var publicKey = getIdentityKeyPair().getPublicKey();
1029 getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
1030 getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
1031 }
1032
1033 @Override
1034 public void close() throws IOException {
1035 synchronized (fileChannel) {
1036 try {
1037 lock.close();
1038 } catch (ClosedChannelException ignored) {
1039 }
1040 fileChannel.close();
1041 }
1042 }
1043 }