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