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