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