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