]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
b1b888ccfaad2c3b1e791074349d2a86c1dc8003
[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 }
248
249 public static File getFileName(File dataPath, String username) {
250 return new File(dataPath, username);
251 }
252
253 private static File getUserPath(final File dataPath, final String username) {
254 return new File(dataPath, username + ".d");
255 }
256
257 public static File getMessageCachePath(File dataPath, String username) {
258 return new File(getUserPath(dataPath, username), "msg-cache");
259 }
260
261 private static File getGroupCachePath(File dataPath, String username) {
262 return new File(getUserPath(dataPath, username), "group-cache");
263 }
264
265 private static File getPreKeysPath(File dataPath, String username) {
266 return new File(getUserPath(dataPath, username), "pre-keys");
267 }
268
269 private static File getSignedPreKeysPath(File dataPath, String username) {
270 return new File(getUserPath(dataPath, username), "signed-pre-keys");
271 }
272
273 private static File getIdentitiesPath(File dataPath, String username) {
274 return new File(getUserPath(dataPath, username), "identities");
275 }
276
277 private static File getSessionsPath(File dataPath, String username) {
278 return new File(getUserPath(dataPath, username), "sessions");
279 }
280
281 private static File getRecipientsStoreFile(File dataPath, String username) {
282 return new File(getUserPath(dataPath, username), "recipients-store");
283 }
284
285 public static boolean userExists(File dataPath, String username) {
286 if (username == null) {
287 return false;
288 }
289 var f = getFileName(dataPath, username);
290 return !(!f.exists() || f.isDirectory());
291 }
292
293 private void load(File dataPath) throws IOException {
294 JsonNode rootNode;
295 synchronized (fileChannel) {
296 fileChannel.position(0);
297 rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
298 }
299
300 if (rootNode.hasNonNull("uuid")) {
301 try {
302 uuid = UUID.fromString(rootNode.get("uuid").asText());
303 } catch (IllegalArgumentException e) {
304 throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e);
305 }
306 }
307 if (rootNode.hasNonNull("deviceId")) {
308 deviceId = rootNode.get("deviceId").asInt();
309 }
310 if (rootNode.hasNonNull("isMultiDevice")) {
311 isMultiDevice = rootNode.get("isMultiDevice").asBoolean();
312 }
313 username = Utils.getNotNullNode(rootNode, "username").asText();
314 password = Utils.getNotNullNode(rootNode, "password").asText();
315 int registrationId = 0;
316 if (rootNode.hasNonNull("registrationId")) {
317 registrationId = rootNode.get("registrationId").asInt();
318 }
319 IdentityKeyPair identityKeyPair = null;
320 if (rootNode.hasNonNull("identityPrivateKey") && rootNode.hasNonNull("identityKey")) {
321 final var publicKeyBytes = Base64.getDecoder().decode(rootNode.get("identityKey").asText());
322 final var privateKeyBytes = Base64.getDecoder().decode(rootNode.get("identityPrivateKey").asText());
323 identityKeyPair = KeyUtils.getIdentityKeyPair(publicKeyBytes, privateKeyBytes);
324 }
325
326 if (rootNode.hasNonNull("registrationLockPin")) {
327 registrationLockPin = rootNode.get("registrationLockPin").asText();
328 }
329 if (rootNode.hasNonNull("pinMasterKey")) {
330 pinMasterKey = new MasterKey(Base64.getDecoder().decode(rootNode.get("pinMasterKey").asText()));
331 }
332 if (rootNode.hasNonNull("storageKey")) {
333 storageKey = new StorageKey(Base64.getDecoder().decode(rootNode.get("storageKey").asText()));
334 }
335 if (rootNode.hasNonNull("preKeyIdOffset")) {
336 preKeyIdOffset = rootNode.get("preKeyIdOffset").asInt(0);
337 } else {
338 preKeyIdOffset = 0;
339 }
340 if (rootNode.hasNonNull("nextSignedPreKeyId")) {
341 nextSignedPreKeyId = rootNode.get("nextSignedPreKeyId").asInt();
342 } else {
343 nextSignedPreKeyId = 0;
344 }
345 if (rootNode.hasNonNull("profileKey")) {
346 try {
347 profileKey = new ProfileKey(Base64.getDecoder().decode(rootNode.get("profileKey").asText()));
348 } catch (InvalidInputException e) {
349 throw new IOException(
350 "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
351 e);
352 }
353 }
354
355 recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
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.resolveRecipients(legacyRecipientStore.getAddresses());
362 }
363 }
364
365 var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
366 ? jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
367 LegacyJsonSignalProtocolStore.class)
368 : null;
369
370 preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
371 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
372 logger.debug("Migrating legacy pre key store.");
373 for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
374 try {
375 preKeyStore.storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
376 } catch (IOException e) {
377 logger.warn("Failed to migrate pre key, ignoring", e);
378 }
379 }
380 }
381
382 signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
383 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
384 logger.debug("Migrating legacy signed pre key store.");
385 for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
386 try {
387 signedPreKeyStore.storeSignedPreKey(entry.getKey(), new SignedPreKeyRecord(entry.getValue()));
388 } catch (IOException e) {
389 logger.warn("Failed to migrate signed pre key, ignoring", e);
390 }
391 }
392 }
393
394 sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient);
395 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySessionStore() != null) {
396 logger.debug("Migrating legacy session store.");
397 for (var session : legacySignalProtocolStore.getLegacySessionStore().getSessions()) {
398 try {
399 sessionStore.storeSession(new SignalProtocolAddress(session.address.getIdentifier(),
400 session.deviceId), new SessionRecord(session.sessionRecord));
401 } catch (IOException e) {
402 logger.warn("Failed to migrate session, ignoring", e);
403 }
404 }
405 }
406
407 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
408 identityKeyPair = legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentityKeyPair();
409 registrationId = legacySignalProtocolStore.getLegacyIdentityKeyStore().getLocalRegistrationId();
410 }
411 identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
412 recipientStore::resolveRecipient,
413 identityKeyPair,
414 registrationId);
415 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
416 logger.debug("Migrating identity session store.");
417 for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
418 RecipientId recipientId = recipientStore.resolveRecipient(identity.getAddress());
419 identityKeyStore.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
420 identityKeyStore.setIdentityTrustLevel(recipientId,
421 identity.getIdentityKey(),
422 identity.getTrustLevel());
423 }
424 }
425
426 signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore);
427
428 registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
429 var groupStoreNode = rootNode.get("groupStore");
430 if (groupStoreNode != null) {
431 groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
432 groupStore.groupCachePath = getGroupCachePath(dataPath, username);
433 }
434 if (groupStore == null) {
435 groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
436 }
437
438 var contactStoreNode = rootNode.get("contactStore");
439 if (contactStoreNode != null) {
440 contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
441 }
442 if (contactStore == null) {
443 contactStore = new JsonContactsStore();
444 }
445
446 var profileStoreNode = rootNode.get("profileStore");
447 if (profileStoreNode != null) {
448 profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class);
449 }
450 if (profileStore == null) {
451 profileStore = new ProfileStore();
452 }
453
454 var stickerStoreNode = rootNode.get("stickerStore");
455 if (stickerStoreNode != null) {
456 stickerStore = jsonProcessor.convertValue(stickerStoreNode, StickerStore.class);
457 }
458 if (stickerStore == null) {
459 stickerStore = new StickerStore();
460 }
461
462 if (recipientStore.isEmpty()) {
463 recipientStore.resolveRecipient(getSelfAddress());
464
465 recipientStore.resolveRecipients(contactStore.getContacts()
466 .stream()
467 .map(ContactInfo::getAddress)
468 .collect(Collectors.toList()));
469
470 for (var group : groupStore.getGroups()) {
471 if (group instanceof GroupInfoV1) {
472 var groupInfoV1 = (GroupInfoV1) group;
473 groupInfoV1.members = groupInfoV1.members.stream()
474 .map(m -> recipientStore.resolveServiceAddress(m))
475 .collect(Collectors.toSet());
476 }
477 }
478 }
479
480 messageCache = new MessageCache(getMessageCachePath(dataPath, username));
481
482 var threadStoreNode = rootNode.get("threadStore");
483 if (threadStoreNode != null && !threadStoreNode.isNull()) {
484 var threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
485 // Migrate thread info to group and contact store
486 for (var thread : threadStore.getThreads()) {
487 if (thread.id == null || thread.id.isEmpty()) {
488 continue;
489 }
490 try {
491 var contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
492 if (contactInfo != null) {
493 contactInfo.messageExpirationTime = thread.messageExpirationTime;
494 contactStore.updateContact(contactInfo);
495 } else {
496 var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
497 if (groupInfo instanceof GroupInfoV1) {
498 ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
499 groupStore.updateGroup(groupInfo);
500 }
501 }
502 } catch (Exception ignored) {
503 }
504 }
505 }
506 }
507
508 public void save() {
509 if (fileChannel == null) {
510 return;
511 }
512 var rootNode = jsonProcessor.createObjectNode();
513 rootNode.put("username", username)
514 .put("uuid", uuid == null ? null : uuid.toString())
515 .put("deviceId", deviceId)
516 .put("isMultiDevice", isMultiDevice)
517 .put("password", password)
518 .put("registrationId", identityKeyStore.getLocalRegistrationId())
519 .put("identityPrivateKey",
520 Base64.getEncoder()
521 .encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
522 .put("identityKey",
523 Base64.getEncoder()
524 .encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
525 .put("registrationLockPin", registrationLockPin)
526 .put("pinMasterKey",
527 pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
528 .put("storageKey",
529 storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
530 .put("preKeyIdOffset", preKeyIdOffset)
531 .put("nextSignedPreKeyId", nextSignedPreKeyId)
532 .put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
533 .put("registered", registered)
534 .putPOJO("groupStore", groupStore)
535 .putPOJO("contactStore", contactStore)
536 .putPOJO("profileStore", profileStore)
537 .putPOJO("stickerStore", stickerStore);
538 try {
539 try (var output = new ByteArrayOutputStream()) {
540 // Write to memory first to prevent corrupting the file in case of serialization errors
541 jsonProcessor.writeValue(output, rootNode);
542 var input = new ByteArrayInputStream(output.toByteArray());
543 synchronized (fileChannel) {
544 fileChannel.position(0);
545 input.transferTo(Channels.newOutputStream(fileChannel));
546 fileChannel.truncate(fileChannel.position());
547 fileChannel.force(false);
548 }
549 }
550 } catch (Exception e) {
551 logger.error("Error saving file: {}", e.getMessage());
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 JsonContactsStore getContactStore() {
605 return contactStore;
606 }
607
608 public RecipientStore getRecipientStore() {
609 return recipientStore;
610 }
611
612 public ProfileStore getProfileStore() {
613 return profileStore;
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 int getDeviceId() {
641 return deviceId;
642 }
643
644 public void setDeviceId(final int deviceId) {
645 this.deviceId = deviceId;
646 }
647
648 public boolean isMasterDevice() {
649 return deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID;
650 }
651
652 public IdentityKeyPair getIdentityKeyPair() {
653 return signalProtocolStore.getIdentityKeyPair();
654 }
655
656 public int getLocalRegistrationId() {
657 return signalProtocolStore.getLocalRegistrationId();
658 }
659
660 public String getPassword() {
661 return password;
662 }
663
664 public void setPassword(final String password) {
665 this.password = password;
666 }
667
668 public String getRegistrationLockPin() {
669 return registrationLockPin;
670 }
671
672 public void setRegistrationLockPin(final String registrationLockPin) {
673 this.registrationLockPin = registrationLockPin;
674 }
675
676 public MasterKey getPinMasterKey() {
677 return pinMasterKey;
678 }
679
680 public void setPinMasterKey(final MasterKey pinMasterKey) {
681 this.pinMasterKey = pinMasterKey;
682 }
683
684 public StorageKey getStorageKey() {
685 if (pinMasterKey != null) {
686 return pinMasterKey.deriveStorageServiceKey();
687 }
688 return storageKey;
689 }
690
691 public void setStorageKey(final StorageKey storageKey) {
692 this.storageKey = storageKey;
693 }
694
695 public ProfileKey getProfileKey() {
696 return profileKey;
697 }
698
699 public void setProfileKey(final ProfileKey profileKey) {
700 this.profileKey = profileKey;
701 }
702
703 public byte[] getSelfUnidentifiedAccessKey() {
704 return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
705 }
706
707 public int getPreKeyIdOffset() {
708 return preKeyIdOffset;
709 }
710
711 public int getNextSignedPreKeyId() {
712 return nextSignedPreKeyId;
713 }
714
715 public boolean isRegistered() {
716 return registered;
717 }
718
719 public void setRegistered(final boolean registered) {
720 this.registered = registered;
721 }
722
723 public boolean isMultiDevice() {
724 return isMultiDevice;
725 }
726
727 public void setMultiDevice(final boolean multiDevice) {
728 isMultiDevice = multiDevice;
729 }
730
731 public boolean isUnrestrictedUnidentifiedAccess() {
732 // TODO make configurable
733 return false;
734 }
735
736 public boolean isDiscoverableByPhoneNumber() {
737 // TODO make configurable
738 return true;
739 }
740
741 @Override
742 public void close() throws IOException {
743 if (fileChannel.isOpen()) {
744 save();
745 }
746 synchronized (fileChannel) {
747 try {
748 lock.close();
749 } catch (ClosedChannelException ignored) {
750 }
751 fileChannel.close();
752 }
753 }
754 }