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