]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
1620dbf258c313ae24055eb7e9b29d74c310a8ea
[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.storeProfileKey(recipientId, profileEntry.getProfileKey());
551 recipientStore.storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential());
552 final var profile = profileEntry.getProfile();
553 if (profile != null) {
554 final var capabilities = new HashSet<Profile.Capability>();
555 if (profile.getCapabilities().gv1Migration) {
556 capabilities.add(Profile.Capability.gv1Migration);
557 }
558 if (profile.getCapabilities().gv2) {
559 capabilities.add(Profile.Capability.gv2);
560 }
561 if (profile.getCapabilities().storage) {
562 capabilities.add(Profile.Capability.storage);
563 }
564 final var newProfile = new Profile(profileEntry.getLastUpdateTimestamp(),
565 profile.getGivenName(),
566 profile.getFamilyName(),
567 profile.getAbout(),
568 profile.getAboutEmoji(),
569 profile.isUnrestrictedUnidentifiedAccess()
570 ? Profile.UnidentifiedAccessMode.UNRESTRICTED
571 : profile.getUnidentifiedAccess() != null
572 ? Profile.UnidentifiedAccessMode.ENABLED
573 : Profile.UnidentifiedAccessMode.DISABLED,
574 capabilities);
575 recipientStore.storeProfile(recipientId, newProfile);
576 }
577 }
578 }
579
580 return migrated;
581 }
582
583 private boolean loadLegacyThreadStore(final JsonNode rootNode) {
584 var threadStoreNode = rootNode.get("threadStore");
585 if (threadStoreNode != null && !threadStoreNode.isNull()) {
586 var threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
587 // Migrate thread info to group and contact store
588 for (var thread : threadStore.getThreads()) {
589 if (thread.id == null || thread.id.isEmpty()) {
590 continue;
591 }
592 try {
593 if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
594 final var recipientId = recipientStore.resolveRecipient(thread.id);
595 var contact = recipientStore.getContact(recipientId);
596 if (contact != null) {
597 recipientStore.storeContact(recipientId,
598 Contact.newBuilder(contact)
599 .withMessageExpirationTime(thread.messageExpirationTime)
600 .build());
601 }
602 } else {
603 var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
604 if (groupInfo instanceof GroupInfoV1) {
605 ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
606 groupStore.updateGroup(groupInfo);
607 }
608 }
609 } catch (Exception e) {
610 logger.warn("Failed to read legacy thread info: {}", e.getMessage());
611 }
612 }
613 return true;
614 }
615
616 return false;
617 }
618
619 private void saveStickerStore(StickerStore.Storage storage) {
620 this.stickerStoreStorage = storage;
621 save();
622 }
623
624 private void saveGroupStore(GroupStore.Storage storage) {
625 this.groupStoreStorage = storage;
626 save();
627 }
628
629 private void save() {
630 synchronized (fileChannel) {
631 var rootNode = jsonProcessor.createObjectNode();
632 rootNode.put("version", CURRENT_STORAGE_VERSION)
633 .put("username", username)
634 .put("uuid", uuid == null ? null : uuid.toString())
635 .put("deviceName", encryptedDeviceName)
636 .put("deviceId", deviceId)
637 .put("isMultiDevice", isMultiDevice)
638 .put("password", password)
639 .put("registrationId", identityKeyStore.getLocalRegistrationId())
640 .put("identityPrivateKey",
641 Base64.getEncoder()
642 .encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
643 .put("identityKey",
644 Base64.getEncoder()
645 .encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
646 .put("registrationLockPin", registrationLockPin)
647 .put("pinMasterKey",
648 pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
649 .put("storageKey",
650 storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
651 .put("preKeyIdOffset", preKeyIdOffset)
652 .put("nextSignedPreKeyId", nextSignedPreKeyId)
653 .put("profileKey",
654 profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
655 .put("registered", registered)
656 .putPOJO("groupStore", groupStoreStorage)
657 .putPOJO("stickerStore", stickerStoreStorage);
658 try {
659 try (var output = new ByteArrayOutputStream()) {
660 // Write to memory first to prevent corrupting the file in case of serialization errors
661 jsonProcessor.writeValue(output, rootNode);
662 var input = new ByteArrayInputStream(output.toByteArray());
663 fileChannel.position(0);
664 input.transferTo(Channels.newOutputStream(fileChannel));
665 fileChannel.truncate(fileChannel.position());
666 fileChannel.force(false);
667 }
668 } catch (Exception e) {
669 logger.error("Error saving file: {}", e.getMessage());
670 }
671 }
672 }
673
674 private static Pair<FileChannel, FileLock> openFileChannel(File fileName, boolean waitForLock) throws IOException {
675 var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
676 var lock = fileChannel.tryLock();
677 if (lock == null) {
678 if (!waitForLock) {
679 logger.debug("Config file is in use by another instance.");
680 throw new IOException("Config file is in use by another instance.");
681 }
682 logger.info("Config file is in use by another instance, waiting…");
683 lock = fileChannel.lock();
684 logger.info("Config file lock acquired.");
685 }
686 return new Pair<>(fileChannel, lock);
687 }
688
689 public void addPreKeys(List<PreKeyRecord> records) {
690 for (var record : records) {
691 if (preKeyIdOffset != record.getId()) {
692 logger.error("Invalid pre key id {}, expected {}", record.getId(), preKeyIdOffset);
693 throw new AssertionError("Invalid pre key id");
694 }
695 preKeyStore.storePreKey(record.getId(), record);
696 preKeyIdOffset = (preKeyIdOffset + 1) % Medium.MAX_VALUE;
697 }
698 save();
699 }
700
701 public void addSignedPreKey(SignedPreKeyRecord record) {
702 if (nextSignedPreKeyId != record.getId()) {
703 logger.error("Invalid signed pre key id {}, expected {}", record.getId(), nextSignedPreKeyId);
704 throw new AssertionError("Invalid signed pre key id");
705 }
706 signalProtocolStore.storeSignedPreKey(record.getId(), record);
707 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
708 save();
709 }
710
711 public SignalProtocolStore getSignalProtocolStore() {
712 return signalProtocolStore;
713 }
714
715 public SessionStore getSessionStore() {
716 return sessionStore;
717 }
718
719 public IdentityKeyStore getIdentityKeyStore() {
720 return identityKeyStore;
721 }
722
723 public GroupStore getGroupStore() {
724 return groupStore;
725 }
726
727 public ContactsStore getContactStore() {
728 return recipientStore;
729 }
730
731 public RecipientStore getRecipientStore() {
732 return recipientStore;
733 }
734
735 public ProfileStore getProfileStore() {
736 return recipientStore;
737 }
738
739 public StickerStore getStickerStore() {
740 return stickerStore;
741 }
742
743 public MessageCache getMessageCache() {
744 return messageCache;
745 }
746
747 public String getUsername() {
748 return username;
749 }
750
751 public UUID getUuid() {
752 return uuid;
753 }
754
755 public void setUuid(final UUID uuid) {
756 this.uuid = uuid;
757 save();
758 }
759
760 public SignalServiceAddress getSelfAddress() {
761 return new SignalServiceAddress(uuid, username);
762 }
763
764 public RecipientId getSelfRecipientId() {
765 return recipientStore.resolveRecipientTrusted(getSelfAddress());
766 }
767
768 public String getEncryptedDeviceName() {
769 return encryptedDeviceName;
770 }
771
772 public int getDeviceId() {
773 return deviceId;
774 }
775
776 public boolean isMasterDevice() {
777 return deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID;
778 }
779
780 public IdentityKeyPair getIdentityKeyPair() {
781 return signalProtocolStore.getIdentityKeyPair();
782 }
783
784 public int getLocalRegistrationId() {
785 return signalProtocolStore.getLocalRegistrationId();
786 }
787
788 public String getPassword() {
789 return password;
790 }
791
792 private void setPassword(final String password) {
793 this.password = password;
794 save();
795 }
796
797 public String getRegistrationLockPin() {
798 return registrationLockPin;
799 }
800
801 public void setRegistrationLockPin(final String registrationLockPin, final MasterKey pinMasterKey) {
802 this.registrationLockPin = registrationLockPin;
803 this.pinMasterKey = pinMasterKey;
804 save();
805 }
806
807 public MasterKey getPinMasterKey() {
808 return pinMasterKey;
809 }
810
811 public StorageKey getStorageKey() {
812 if (pinMasterKey != null) {
813 return pinMasterKey.deriveStorageServiceKey();
814 }
815 return storageKey;
816 }
817
818 public void setStorageKey(final StorageKey storageKey) {
819 if (storageKey.equals(this.storageKey)) {
820 return;
821 }
822 this.storageKey = storageKey;
823 save();
824 }
825
826 public ProfileKey getProfileKey() {
827 return profileKey;
828 }
829
830 public void setProfileKey(final ProfileKey profileKey) {
831 if (profileKey.equals(this.profileKey)) {
832 return;
833 }
834 this.profileKey = profileKey;
835 save();
836 }
837
838 public byte[] getSelfUnidentifiedAccessKey() {
839 return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
840 }
841
842 public int getPreKeyIdOffset() {
843 return preKeyIdOffset;
844 }
845
846 public int getNextSignedPreKeyId() {
847 return nextSignedPreKeyId;
848 }
849
850 public boolean isRegistered() {
851 return registered;
852 }
853
854 public void setRegistered(final boolean registered) {
855 this.registered = registered;
856 save();
857 }
858
859 public boolean isMultiDevice() {
860 return isMultiDevice;
861 }
862
863 public void setMultiDevice(final boolean multiDevice) {
864 if (isMultiDevice == multiDevice) {
865 return;
866 }
867 isMultiDevice = multiDevice;
868 save();
869 }
870
871 public boolean isUnrestrictedUnidentifiedAccess() {
872 // TODO make configurable
873 return false;
874 }
875
876 public boolean isDiscoverableByPhoneNumber() {
877 // TODO make configurable
878 return true;
879 }
880
881 public void finishRegistration(final UUID uuid, final MasterKey masterKey, final String pin) {
882 this.pinMasterKey = masterKey;
883 this.encryptedDeviceName = null;
884 this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
885 this.isMultiDevice = false;
886 this.registered = true;
887 this.uuid = uuid;
888 this.registrationLockPin = pin;
889 save();
890
891 getSessionStore().archiveAllSessions();
892 final var recipientId = getRecipientStore().resolveRecipientTrusted(getSelfAddress());
893 final var publicKey = getIdentityKeyPair().getPublicKey();
894 getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
895 getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
896 }
897
898 @Override
899 public void close() throws IOException {
900 synchronized (fileChannel) {
901 try {
902 lock.close();
903 } catch (ClosedChannelException ignored) {
904 }
905 fileChannel.close();
906 }
907 }
908 }