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