]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
9c51017c4bcd3e64f8be7e36765570dd204fe809
[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 profile.isUnrestrictedUnidentifiedAccess()
635 ? Profile.UnidentifiedAccessMode.UNRESTRICTED
636 : profile.getUnidentifiedAccess() != null
637 ? Profile.UnidentifiedAccessMode.ENABLED
638 : Profile.UnidentifiedAccessMode.DISABLED,
639 capabilities);
640 recipientStore.storeProfile(recipientId, newProfile);
641 }
642 }
643 }
644
645 return migrated;
646 }
647
648 private boolean loadLegacyThreadStore(final JsonNode rootNode) {
649 var threadStoreNode = rootNode.get("threadStore");
650 if (threadStoreNode != null && !threadStoreNode.isNull()) {
651 var threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
652 // Migrate thread info to group and contact store
653 for (var thread : threadStore.getThreads()) {
654 if (thread.id == null || thread.id.isEmpty()) {
655 continue;
656 }
657 try {
658 if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
659 final var recipientId = recipientStore.resolveRecipient(thread.id);
660 var contact = recipientStore.getContact(recipientId);
661 if (contact != null) {
662 recipientStore.storeContact(recipientId,
663 Contact.newBuilder(contact)
664 .withMessageExpirationTime(thread.messageExpirationTime)
665 .build());
666 }
667 } else {
668 var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
669 if (groupInfo instanceof GroupInfoV1) {
670 ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
671 groupStore.updateGroup(groupInfo);
672 }
673 }
674 } catch (Exception e) {
675 logger.warn("Failed to read legacy thread info: {}", e.getMessage());
676 }
677 }
678 return true;
679 }
680
681 return false;
682 }
683
684 private void saveStickerStore(StickerStore.Storage storage) {
685 this.stickerStoreStorage = storage;
686 save();
687 }
688
689 private void saveGroupStore(GroupStore.Storage storage) {
690 this.groupStoreStorage = storage;
691 save();
692 }
693
694 private void saveConfigurationStore(ConfigurationStore.Storage storage) {
695 this.configurationStoreStorage = storage;
696 save();
697 }
698
699 private void save() {
700 synchronized (fileChannel) {
701 var rootNode = jsonProcessor.createObjectNode();
702 rootNode.put("version", CURRENT_STORAGE_VERSION)
703 .put("username", username)
704 .put("uuid", uuid == null ? null : uuid.toString())
705 .put("deviceName", encryptedDeviceName)
706 .put("deviceId", deviceId)
707 .put("isMultiDevice", isMultiDevice)
708 .put("lastReceiveTimestamp", lastReceiveTimestamp)
709 .put("password", password)
710 .put("registrationId", identityKeyStore.getLocalRegistrationId())
711 .put("identityPrivateKey",
712 Base64.getEncoder()
713 .encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
714 .put("identityKey",
715 Base64.getEncoder()
716 .encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
717 .put("registrationLockPin", registrationLockPin)
718 .put("pinMasterKey",
719 pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
720 .put("storageKey",
721 storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
722 .put("storageManifestVersion", storageManifestVersion == -1 ? null : storageManifestVersion)
723 .put("preKeyIdOffset", preKeyIdOffset)
724 .put("nextSignedPreKeyId", nextSignedPreKeyId)
725 .put("profileKey",
726 profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
727 .put("registered", registered)
728 .putPOJO("groupStore", groupStoreStorage)
729 .putPOJO("stickerStore", stickerStoreStorage)
730 .putPOJO("configurationStore", configurationStoreStorage);
731 try {
732 try (var output = new ByteArrayOutputStream()) {
733 // Write to memory first to prevent corrupting the file in case of serialization errors
734 jsonProcessor.writeValue(output, rootNode);
735 var input = new ByteArrayInputStream(output.toByteArray());
736 fileChannel.position(0);
737 input.transferTo(Channels.newOutputStream(fileChannel));
738 fileChannel.truncate(fileChannel.position());
739 fileChannel.force(false);
740 }
741 } catch (Exception e) {
742 logger.error("Error saving file: {}", e.getMessage());
743 }
744 }
745 }
746
747 private static Pair<FileChannel, FileLock> openFileChannel(File fileName, boolean waitForLock) throws IOException {
748 var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
749 var lock = fileChannel.tryLock();
750 if (lock == null) {
751 if (!waitForLock) {
752 logger.debug("Config file is in use by another instance.");
753 throw new IOException("Config file is in use by another instance.");
754 }
755 logger.info("Config file is in use by another instance, waiting…");
756 lock = fileChannel.lock();
757 logger.info("Config file lock acquired.");
758 }
759 return new Pair<>(fileChannel, lock);
760 }
761
762 public void addPreKeys(List<PreKeyRecord> records) {
763 for (var record : records) {
764 if (preKeyIdOffset != record.getId()) {
765 logger.error("Invalid pre key id {}, expected {}", record.getId(), preKeyIdOffset);
766 throw new AssertionError("Invalid pre key id");
767 }
768 preKeyStore.storePreKey(record.getId(), record);
769 preKeyIdOffset = (preKeyIdOffset + 1) % Medium.MAX_VALUE;
770 }
771 save();
772 }
773
774 public void addSignedPreKey(SignedPreKeyRecord record) {
775 if (nextSignedPreKeyId != record.getId()) {
776 logger.error("Invalid signed pre key id {}, expected {}", record.getId(), nextSignedPreKeyId);
777 throw new AssertionError("Invalid signed pre key id");
778 }
779 signalProtocolStore.storeSignedPreKey(record.getId(), record);
780 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
781 save();
782 }
783
784 public SignalProtocolStore getSignalProtocolStore() {
785 return signalProtocolStore;
786 }
787
788 public SessionStore getSessionStore() {
789 return sessionStore;
790 }
791
792 public IdentityKeyStore getIdentityKeyStore() {
793 return identityKeyStore;
794 }
795
796 public GroupStore getGroupStore() {
797 return groupStore;
798 }
799
800 public ContactsStore getContactStore() {
801 return recipientStore;
802 }
803
804 public RecipientStore getRecipientStore() {
805 return recipientStore;
806 }
807
808 public ProfileStore getProfileStore() {
809 return recipientStore;
810 }
811
812 public StickerStore getStickerStore() {
813 return stickerStore;
814 }
815
816 public SenderKeyStore getSenderKeyStore() {
817 return senderKeyStore;
818 }
819
820 public ConfigurationStore getConfigurationStore() {
821 return configurationStore;
822 }
823
824 public MessageCache getMessageCache() {
825 return messageCache;
826 }
827
828 public String getUsername() {
829 return username;
830 }
831
832 public UUID getUuid() {
833 return uuid;
834 }
835
836 public void setUuid(final UUID uuid) {
837 this.uuid = uuid;
838 save();
839 }
840
841 public SignalServiceAddress getSelfAddress() {
842 return new SignalServiceAddress(uuid, username);
843 }
844
845 public RecipientId getSelfRecipientId() {
846 return recipientStore.resolveRecipientTrusted(new RecipientAddress(uuid, username));
847 }
848
849 public String getEncryptedDeviceName() {
850 return encryptedDeviceName;
851 }
852
853 public void setEncryptedDeviceName(final String encryptedDeviceName) {
854 this.encryptedDeviceName = encryptedDeviceName;
855 save();
856 }
857
858 public int getDeviceId() {
859 return deviceId;
860 }
861
862 public boolean isMasterDevice() {
863 return deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID;
864 }
865
866 public IdentityKeyPair getIdentityKeyPair() {
867 return signalProtocolStore.getIdentityKeyPair();
868 }
869
870 public int getLocalRegistrationId() {
871 return signalProtocolStore.getLocalRegistrationId();
872 }
873
874 public String getPassword() {
875 return password;
876 }
877
878 private void setPassword(final String password) {
879 this.password = password;
880 save();
881 }
882
883 public String getRegistrationLockPin() {
884 return registrationLockPin;
885 }
886
887 public void setRegistrationLockPin(final String registrationLockPin, final MasterKey pinMasterKey) {
888 this.registrationLockPin = registrationLockPin;
889 this.pinMasterKey = pinMasterKey;
890 save();
891 }
892
893 public MasterKey getPinMasterKey() {
894 return pinMasterKey;
895 }
896
897 public StorageKey getStorageKey() {
898 if (pinMasterKey != null) {
899 return pinMasterKey.deriveStorageServiceKey();
900 }
901 return storageKey;
902 }
903
904 public void setStorageKey(final StorageKey storageKey) {
905 if (storageKey.equals(this.storageKey)) {
906 return;
907 }
908 this.storageKey = storageKey;
909 save();
910 }
911
912 public long getStorageManifestVersion() {
913 return this.storageManifestVersion;
914 }
915
916 public void setStorageManifestVersion(final long storageManifestVersion) {
917 if (storageManifestVersion == this.storageManifestVersion) {
918 return;
919 }
920 this.storageManifestVersion = storageManifestVersion;
921 save();
922 }
923
924 public ProfileKey getProfileKey() {
925 return profileKey;
926 }
927
928 public void setProfileKey(final ProfileKey profileKey) {
929 if (profileKey.equals(this.profileKey)) {
930 return;
931 }
932 this.profileKey = profileKey;
933 save();
934 }
935
936 public byte[] getSelfUnidentifiedAccessKey() {
937 return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
938 }
939
940 public int getPreKeyIdOffset() {
941 return preKeyIdOffset;
942 }
943
944 public int getNextSignedPreKeyId() {
945 return nextSignedPreKeyId;
946 }
947
948 public boolean isRegistered() {
949 return registered;
950 }
951
952 public void setRegistered(final boolean registered) {
953 this.registered = registered;
954 save();
955 }
956
957 public boolean isMultiDevice() {
958 return isMultiDevice;
959 }
960
961 public void setMultiDevice(final boolean multiDevice) {
962 if (isMultiDevice == multiDevice) {
963 return;
964 }
965 isMultiDevice = multiDevice;
966 save();
967 }
968
969 public long getLastReceiveTimestamp() {
970 return lastReceiveTimestamp;
971 }
972
973 public void setLastReceiveTimestamp(final long lastReceiveTimestamp) {
974 this.lastReceiveTimestamp = lastReceiveTimestamp;
975 save();
976 }
977
978 public boolean isUnrestrictedUnidentifiedAccess() {
979 // TODO make configurable
980 return false;
981 }
982
983 public boolean isDiscoverableByPhoneNumber() {
984 // TODO make configurable
985 return true;
986 }
987
988 public boolean isPhoneNumberShared() {
989 // TODO make configurable
990 return true;
991 }
992
993 public void finishRegistration(final UUID uuid, final MasterKey masterKey, final String pin) {
994 this.pinMasterKey = masterKey;
995 this.storageManifestVersion = -1;
996 this.storageKey = null;
997 this.encryptedDeviceName = null;
998 this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
999 this.isMultiDevice = false;
1000 this.registered = true;
1001 this.uuid = uuid;
1002 this.registrationLockPin = pin;
1003 this.lastReceiveTimestamp = 0;
1004 save();
1005
1006 getSessionStore().archiveAllSessions();
1007 senderKeyStore.deleteAll();
1008 final var recipientId = getRecipientStore().resolveRecipientTrusted(getSelfAddress());
1009 final var publicKey = getIdentityKeyPair().getPublicKey();
1010 getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
1011 getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
1012 }
1013
1014 @Override
1015 public void close() throws IOException {
1016 synchronized (fileChannel) {
1017 try {
1018 lock.close();
1019 } catch (ClosedChannelException ignored) {
1020 }
1021 fileChannel.close();
1022 }
1023 }
1024 }