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