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