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