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