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