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