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