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