]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
c972c2c7a809458c103355c40f18b751dc40331d
[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,
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);
178 identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
179 recipientStore,
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,
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,
457 this::saveGroupStore);
458 } else {
459 groupStore = new GroupStore(getGroupCachePath(dataPath, username), recipientStore, this::saveGroupStore);
460 }
461
462 if (rootNode.hasNonNull("stickerStore")) {
463 stickerStoreStorage = jsonProcessor.convertValue(rootNode.get("stickerStore"), StickerStore.Storage.class);
464 stickerStore = StickerStore.fromStorage(stickerStoreStorage, this::saveStickerStore);
465 } else {
466 stickerStore = new StickerStore(this::saveStickerStore);
467 }
468
469 migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig;
470
471 if (migratedLegacyConfig) {
472 save();
473 }
474 }
475
476 private boolean loadLegacyStores(
477 final JsonNode rootNode, final LegacyJsonSignalProtocolStore legacySignalProtocolStore
478 ) {
479 var migrated = false;
480 var legacyRecipientStoreNode = rootNode.get("recipientStore");
481 if (legacyRecipientStoreNode != null) {
482 logger.debug("Migrating legacy recipient store.");
483 var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
484 if (legacyRecipientStore != null) {
485 recipientStore.resolveRecipientsTrusted(legacyRecipientStore.getAddresses());
486 }
487 recipientStore.resolveRecipientTrusted(getSelfAddress());
488 migrated = true;
489 }
490
491 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
492 logger.debug("Migrating legacy pre key store.");
493 for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
494 try {
495 preKeyStore.storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
496 } catch (IOException e) {
497 logger.warn("Failed to migrate pre key, ignoring", e);
498 }
499 }
500 migrated = true;
501 }
502
503 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
504 logger.debug("Migrating legacy signed pre key store.");
505 for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
506 try {
507 signedPreKeyStore.storeSignedPreKey(entry.getKey(), new SignedPreKeyRecord(entry.getValue()));
508 } catch (IOException e) {
509 logger.warn("Failed to migrate signed pre key, ignoring", e);
510 }
511 }
512 migrated = true;
513 }
514
515 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySessionStore() != null) {
516 logger.debug("Migrating legacy session store.");
517 for (var session : legacySignalProtocolStore.getLegacySessionStore().getSessions()) {
518 try {
519 sessionStore.storeSession(new SignalProtocolAddress(session.address.getIdentifier(),
520 session.deviceId), new SessionRecord(session.sessionRecord));
521 } catch (IOException e) {
522 logger.warn("Failed to migrate session, ignoring", e);
523 }
524 }
525 migrated = true;
526 }
527
528 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
529 logger.debug("Migrating legacy identity session store.");
530 for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
531 RecipientId recipientId = recipientStore.resolveRecipientTrusted(identity.getAddress());
532 identityKeyStore.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
533 identityKeyStore.setIdentityTrustLevel(recipientId,
534 identity.getIdentityKey(),
535 identity.getTrustLevel());
536 }
537 migrated = true;
538 }
539
540 if (rootNode.hasNonNull("contactStore")) {
541 logger.debug("Migrating legacy contact store.");
542 final var contactStoreNode = rootNode.get("contactStore");
543 final var contactStore = jsonProcessor.convertValue(contactStoreNode, LegacyJsonContactsStore.class);
544 for (var contact : contactStore.getContacts()) {
545 final var recipientId = recipientStore.resolveRecipientTrusted(contact.getAddress());
546 recipientStore.storeContact(recipientId,
547 new Contact(contact.name,
548 contact.color,
549 contact.messageExpirationTime,
550 contact.blocked,
551 contact.archived));
552
553 // Store profile keys only in profile store
554 var profileKeyString = contact.profileKey;
555 if (profileKeyString != null) {
556 final ProfileKey profileKey;
557 try {
558 profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
559 getProfileStore().storeProfileKey(recipientId, profileKey);
560 } catch (InvalidInputException e) {
561 logger.warn("Failed to parse legacy contact profile key: {}", e.getMessage());
562 }
563 }
564 }
565 migrated = true;
566 }
567
568 if (rootNode.hasNonNull("profileStore")) {
569 logger.debug("Migrating legacy profile store.");
570 var profileStoreNode = rootNode.get("profileStore");
571 final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class);
572 for (var profileEntry : legacyProfileStore.getProfileEntries()) {
573 var recipientId = recipientStore.resolveRecipient(profileEntry.getAddress());
574 recipientStore.storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential());
575 recipientStore.storeProfileKey(recipientId, profileEntry.getProfileKey());
576 final var profile = profileEntry.getProfile();
577 if (profile != null) {
578 final var capabilities = new HashSet<Profile.Capability>();
579 if (profile.getCapabilities() != null) {
580 if (profile.getCapabilities().gv1Migration) {
581 capabilities.add(Profile.Capability.gv1Migration);
582 }
583 if (profile.getCapabilities().gv2) {
584 capabilities.add(Profile.Capability.gv2);
585 }
586 if (profile.getCapabilities().storage) {
587 capabilities.add(Profile.Capability.storage);
588 }
589 }
590 final var newProfile = new Profile(profileEntry.getLastUpdateTimestamp(),
591 profile.getGivenName(),
592 profile.getFamilyName(),
593 profile.getAbout(),
594 profile.getAboutEmoji(),
595 profile.isUnrestrictedUnidentifiedAccess()
596 ? Profile.UnidentifiedAccessMode.UNRESTRICTED
597 : profile.getUnidentifiedAccess() != null
598 ? Profile.UnidentifiedAccessMode.ENABLED
599 : Profile.UnidentifiedAccessMode.DISABLED,
600 capabilities);
601 recipientStore.storeProfile(recipientId, newProfile);
602 }
603 }
604 }
605
606 return migrated;
607 }
608
609 private boolean loadLegacyThreadStore(final JsonNode rootNode) {
610 var threadStoreNode = rootNode.get("threadStore");
611 if (threadStoreNode != null && !threadStoreNode.isNull()) {
612 var threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
613 // Migrate thread info to group and contact store
614 for (var thread : threadStore.getThreads()) {
615 if (thread.id == null || thread.id.isEmpty()) {
616 continue;
617 }
618 try {
619 if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
620 final var recipientId = recipientStore.resolveRecipient(thread.id);
621 var contact = recipientStore.getContact(recipientId);
622 if (contact != null) {
623 recipientStore.storeContact(recipientId,
624 Contact.newBuilder(contact)
625 .withMessageExpirationTime(thread.messageExpirationTime)
626 .build());
627 }
628 } else {
629 var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
630 if (groupInfo instanceof GroupInfoV1) {
631 ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
632 groupStore.updateGroup(groupInfo);
633 }
634 }
635 } catch (Exception e) {
636 logger.warn("Failed to read legacy thread info: {}", e.getMessage());
637 }
638 }
639 return true;
640 }
641
642 return false;
643 }
644
645 private void saveStickerStore(StickerStore.Storage storage) {
646 this.stickerStoreStorage = storage;
647 save();
648 }
649
650 private void saveGroupStore(GroupStore.Storage storage) {
651 this.groupStoreStorage = storage;
652 save();
653 }
654
655 private void save() {
656 synchronized (fileChannel) {
657 var rootNode = jsonProcessor.createObjectNode();
658 rootNode.put("version", CURRENT_STORAGE_VERSION)
659 .put("username", username)
660 .put("uuid", uuid == null ? null : uuid.toString())
661 .put("deviceName", encryptedDeviceName)
662 .put("deviceId", deviceId)
663 .put("isMultiDevice", isMultiDevice)
664 .put("lastReceiveTimestamp", lastReceiveTimestamp)
665 .put("password", password)
666 .put("registrationId", identityKeyStore.getLocalRegistrationId())
667 .put("identityPrivateKey",
668 Base64.getEncoder()
669 .encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
670 .put("identityKey",
671 Base64.getEncoder()
672 .encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
673 .put("registrationLockPin", registrationLockPin)
674 .put("pinMasterKey",
675 pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
676 .put("storageKey",
677 storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
678 .put("preKeyIdOffset", preKeyIdOffset)
679 .put("nextSignedPreKeyId", nextSignedPreKeyId)
680 .put("profileKey",
681 profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
682 .put("registered", registered)
683 .putPOJO("groupStore", groupStoreStorage)
684 .putPOJO("stickerStore", stickerStoreStorage);
685 try {
686 try (var output = new ByteArrayOutputStream()) {
687 // Write to memory first to prevent corrupting the file in case of serialization errors
688 jsonProcessor.writeValue(output, rootNode);
689 var input = new ByteArrayInputStream(output.toByteArray());
690 fileChannel.position(0);
691 input.transferTo(Channels.newOutputStream(fileChannel));
692 fileChannel.truncate(fileChannel.position());
693 fileChannel.force(false);
694 }
695 } catch (Exception e) {
696 logger.error("Error saving file: {}", e.getMessage());
697 }
698 }
699 }
700
701 private static Pair<FileChannel, FileLock> openFileChannel(File fileName, boolean waitForLock) throws IOException {
702 var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
703 var lock = fileChannel.tryLock();
704 if (lock == null) {
705 if (!waitForLock) {
706 logger.debug("Config file is in use by another instance.");
707 throw new IOException("Config file is in use by another instance.");
708 }
709 logger.info("Config file is in use by another instance, waiting…");
710 lock = fileChannel.lock();
711 logger.info("Config file lock acquired.");
712 }
713 return new Pair<>(fileChannel, lock);
714 }
715
716 public void addPreKeys(List<PreKeyRecord> records) {
717 for (var record : records) {
718 if (preKeyIdOffset != record.getId()) {
719 logger.error("Invalid pre key id {}, expected {}", record.getId(), preKeyIdOffset);
720 throw new AssertionError("Invalid pre key id");
721 }
722 preKeyStore.storePreKey(record.getId(), record);
723 preKeyIdOffset = (preKeyIdOffset + 1) % Medium.MAX_VALUE;
724 }
725 save();
726 }
727
728 public void addSignedPreKey(SignedPreKeyRecord record) {
729 if (nextSignedPreKeyId != record.getId()) {
730 logger.error("Invalid signed pre key id {}, expected {}", record.getId(), nextSignedPreKeyId);
731 throw new AssertionError("Invalid signed pre key id");
732 }
733 signalProtocolStore.storeSignedPreKey(record.getId(), record);
734 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
735 save();
736 }
737
738 public SignalProtocolStore getSignalProtocolStore() {
739 return signalProtocolStore;
740 }
741
742 public SessionStore getSessionStore() {
743 return sessionStore;
744 }
745
746 public IdentityKeyStore getIdentityKeyStore() {
747 return identityKeyStore;
748 }
749
750 public GroupStore getGroupStore() {
751 return groupStore;
752 }
753
754 public ContactsStore getContactStore() {
755 return recipientStore;
756 }
757
758 public RecipientStore getRecipientStore() {
759 return recipientStore;
760 }
761
762 public ProfileStore getProfileStore() {
763 return recipientStore;
764 }
765
766 public StickerStore getStickerStore() {
767 return stickerStore;
768 }
769
770 public MessageCache getMessageCache() {
771 return messageCache;
772 }
773
774 public String getUsername() {
775 return username;
776 }
777
778 public UUID getUuid() {
779 return uuid;
780 }
781
782 public void setUuid(final UUID uuid) {
783 this.uuid = uuid;
784 save();
785 }
786
787 public SignalServiceAddress getSelfAddress() {
788 return new SignalServiceAddress(uuid, username);
789 }
790
791 public RecipientId getSelfRecipientId() {
792 return recipientStore.resolveRecipientTrusted(getSelfAddress());
793 }
794
795 public String getEncryptedDeviceName() {
796 return encryptedDeviceName;
797 }
798
799 public int getDeviceId() {
800 return deviceId;
801 }
802
803 public boolean isMasterDevice() {
804 return deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID;
805 }
806
807 public IdentityKeyPair getIdentityKeyPair() {
808 return signalProtocolStore.getIdentityKeyPair();
809 }
810
811 public int getLocalRegistrationId() {
812 return signalProtocolStore.getLocalRegistrationId();
813 }
814
815 public String getPassword() {
816 return password;
817 }
818
819 private void setPassword(final String password) {
820 this.password = password;
821 save();
822 }
823
824 public String getRegistrationLockPin() {
825 return registrationLockPin;
826 }
827
828 public void setRegistrationLockPin(final String registrationLockPin, final MasterKey pinMasterKey) {
829 this.registrationLockPin = registrationLockPin;
830 this.pinMasterKey = pinMasterKey;
831 save();
832 }
833
834 public MasterKey getPinMasterKey() {
835 return pinMasterKey;
836 }
837
838 public StorageKey getStorageKey() {
839 if (pinMasterKey != null) {
840 return pinMasterKey.deriveStorageServiceKey();
841 }
842 return storageKey;
843 }
844
845 public void setStorageKey(final StorageKey storageKey) {
846 if (storageKey.equals(this.storageKey)) {
847 return;
848 }
849 this.storageKey = storageKey;
850 save();
851 }
852
853 public ProfileKey getProfileKey() {
854 return profileKey;
855 }
856
857 public void setProfileKey(final ProfileKey profileKey) {
858 if (profileKey.equals(this.profileKey)) {
859 return;
860 }
861 this.profileKey = profileKey;
862 save();
863 }
864
865 public byte[] getSelfUnidentifiedAccessKey() {
866 return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
867 }
868
869 public int getPreKeyIdOffset() {
870 return preKeyIdOffset;
871 }
872
873 public int getNextSignedPreKeyId() {
874 return nextSignedPreKeyId;
875 }
876
877 public boolean isRegistered() {
878 return registered;
879 }
880
881 public void setRegistered(final boolean registered) {
882 this.registered = registered;
883 save();
884 }
885
886 public boolean isMultiDevice() {
887 return isMultiDevice;
888 }
889
890 public void setMultiDevice(final boolean multiDevice) {
891 if (isMultiDevice == multiDevice) {
892 return;
893 }
894 isMultiDevice = multiDevice;
895 save();
896 }
897
898 public long getLastReceiveTimestamp() {
899 return lastReceiveTimestamp;
900 }
901
902 public void setLastReceiveTimestamp(final long lastReceiveTimestamp) {
903 this.lastReceiveTimestamp = lastReceiveTimestamp;
904 save();
905 }
906
907 public boolean isUnrestrictedUnidentifiedAccess() {
908 // TODO make configurable
909 return false;
910 }
911
912 public boolean isDiscoverableByPhoneNumber() {
913 // TODO make configurable
914 return true;
915 }
916
917 public boolean isPhoneNumberShared() {
918 // TODO make configurable
919 return true;
920 }
921
922 public void finishRegistration(final UUID uuid, final MasterKey masterKey, final String pin) {
923 this.pinMasterKey = masterKey;
924 this.encryptedDeviceName = null;
925 this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
926 this.isMultiDevice = false;
927 this.registered = true;
928 this.uuid = uuid;
929 this.registrationLockPin = pin;
930 this.lastReceiveTimestamp = 0;
931 save();
932
933 getSessionStore().archiveAllSessions();
934 final var recipientId = getRecipientStore().resolveRecipientTrusted(getSelfAddress());
935 final var publicKey = getIdentityKeyPair().getPublicKey();
936 getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
937 getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
938 }
939
940 @Override
941 public void close() throws IOException {
942 synchronized (fileChannel) {
943 try {
944 lock.close();
945 } catch (ClosedChannelException ignored) {
946 }
947 fileChannel.close();
948 }
949 }
950 }