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