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