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