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