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