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