]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
1ab383dfec5aba5679362d33dfd47405678d9e79
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / SignalAccount.java
1 package org.asamk.signal.manager.storage;
2
3 import com.fasterxml.jackson.databind.JsonNode;
4 import com.fasterxml.jackson.databind.ObjectMapper;
5
6 import org.asamk.signal.manager.api.Pair;
7 import org.asamk.signal.manager.api.TrustLevel;
8 import org.asamk.signal.manager.groups.GroupId;
9 import org.asamk.signal.manager.storage.configuration.ConfigurationStore;
10 import org.asamk.signal.manager.storage.contacts.ContactsStore;
11 import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
12 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
13 import org.asamk.signal.manager.storage.groups.GroupInfoV2;
14 import org.asamk.signal.manager.storage.groups.GroupStore;
15 import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
16 import org.asamk.signal.manager.storage.identities.TrustNewIdentity;
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.LegacyProfileStore;
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.Contact;
25 import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
26 import org.asamk.signal.manager.storage.recipients.Profile;
27 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
28 import org.asamk.signal.manager.storage.recipients.RecipientId;
29 import org.asamk.signal.manager.storage.recipients.RecipientStore;
30 import org.asamk.signal.manager.storage.sendLog.MessageSendLogStore;
31 import org.asamk.signal.manager.storage.senderKeys.SenderKeyStore;
32 import org.asamk.signal.manager.storage.sessions.SessionStore;
33 import org.asamk.signal.manager.storage.stickers.StickerStore;
34 import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
35 import org.asamk.signal.manager.util.IOUtils;
36 import org.asamk.signal.manager.util.KeyUtils;
37 import org.signal.libsignal.protocol.IdentityKeyPair;
38 import org.signal.libsignal.protocol.InvalidMessageException;
39 import org.signal.libsignal.protocol.SignalProtocolAddress;
40 import org.signal.libsignal.protocol.state.PreKeyRecord;
41 import org.signal.libsignal.protocol.state.SessionRecord;
42 import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
43 import org.signal.libsignal.protocol.util.Medium;
44 import org.signal.libsignal.zkgroup.InvalidInputException;
45 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
48 import org.whispersystems.signalservice.api.SignalServiceAccountDataStore;
49 import org.whispersystems.signalservice.api.SignalServiceDataStore;
50 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
51 import org.whispersystems.signalservice.api.kbs.MasterKey;
52 import org.whispersystems.signalservice.api.push.ACI;
53 import org.whispersystems.signalservice.api.push.DistributionId;
54 import org.whispersystems.signalservice.api.push.PNI;
55 import org.whispersystems.signalservice.api.push.ServiceId;
56 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
57 import org.whispersystems.signalservice.api.storage.StorageKey;
58 import org.whispersystems.signalservice.api.util.CredentialsProvider;
59 import org.whispersystems.signalservice.api.util.UuidUtil;
60
61 import java.io.ByteArrayInputStream;
62 import java.io.ByteArrayOutputStream;
63 import java.io.Closeable;
64 import java.io.File;
65 import java.io.IOException;
66 import java.io.RandomAccessFile;
67 import java.nio.channels.Channels;
68 import java.nio.channels.ClosedChannelException;
69 import java.nio.channels.FileChannel;
70 import java.nio.channels.FileLock;
71 import java.security.SecureRandom;
72 import java.sql.SQLException;
73 import java.util.Base64;
74 import java.util.Date;
75 import java.util.HashSet;
76 import java.util.List;
77 import java.util.function.Supplier;
78
79 public class SignalAccount implements Closeable {
80
81 private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
82
83 private static final int MINIMUM_STORAGE_VERSION = 1;
84 private static final int CURRENT_STORAGE_VERSION = 3;
85
86 private final Object LOCK = new Object();
87
88 private final ObjectMapper jsonProcessor = Utils.createStorageObjectMapper();
89
90 private final FileChannel fileChannel;
91 private final FileLock lock;
92
93 private int previousStorageVersion;
94
95 private File dataPath;
96 private String accountPath;
97 private String number;
98 private ACI aci;
99 private PNI pni;
100 private String encryptedDeviceName;
101 private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
102 private boolean isMultiDevice = false;
103 private String password;
104 private String registrationLockPin;
105 private MasterKey pinMasterKey;
106 private StorageKey storageKey;
107 private long storageManifestVersion = -1;
108 private ProfileKey profileKey;
109 private int preKeyIdOffset = 1;
110 private int nextSignedPreKeyId = 1;
111 private IdentityKeyPair aciIdentityKeyPair;
112 private IdentityKeyPair pniIdentityKeyPair;
113 private int localRegistrationId;
114 private TrustNewIdentity trustNewIdentity;
115 private long lastReceiveTimestamp = 0;
116
117 private boolean registered = false;
118
119 private SignalProtocolStore signalProtocolStore;
120 private PreKeyStore preKeyStore;
121 private SignedPreKeyStore signedPreKeyStore;
122 private SessionStore sessionStore;
123 private IdentityKeyStore identityKeyStore;
124 private SenderKeyStore senderKeyStore;
125 private GroupStore groupStore;
126 private GroupStore.Storage groupStoreStorage;
127 private RecipientStore recipientStore;
128 private StickerStore stickerStore;
129 private StickerStore.Storage stickerStoreStorage;
130 private ConfigurationStore configurationStore;
131 private ConfigurationStore.Storage configurationStoreStorage;
132
133 private MessageCache messageCache;
134 private MessageSendLogStore messageSendLogStore;
135
136 private AccountDatabase accountDatabase;
137
138 private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
139 this.fileChannel = fileChannel;
140 this.lock = lock;
141 }
142
143 public static SignalAccount load(
144 File dataPath, String accountPath, boolean waitForLock, final TrustNewIdentity trustNewIdentity
145 ) throws IOException {
146 logger.trace("Opening account file");
147 final var fileName = getFileName(dataPath, accountPath);
148 final var pair = openFileChannel(fileName, waitForLock);
149 try {
150 var signalAccount = new SignalAccount(pair.first(), pair.second());
151 logger.trace("Loading account file");
152 signalAccount.load(dataPath, accountPath, trustNewIdentity);
153 logger.trace("Migrating legacy parts of account file");
154 signalAccount.migrateLegacyConfigs();
155
156 return signalAccount;
157 } catch (Throwable e) {
158 pair.second().close();
159 pair.first().close();
160 throw e;
161 }
162 }
163
164 public static SignalAccount create(
165 File dataPath,
166 String accountPath,
167 String number,
168 IdentityKeyPair aciIdentityKey,
169 IdentityKeyPair pniIdentityKey,
170 int registrationId,
171 ProfileKey profileKey,
172 final TrustNewIdentity trustNewIdentity
173 ) throws IOException {
174 IOUtils.createPrivateDirectories(dataPath);
175 var fileName = getFileName(dataPath, accountPath);
176 if (!fileName.exists()) {
177 IOUtils.createPrivateFile(fileName);
178 }
179
180 final var pair = openFileChannel(fileName, true);
181 var signalAccount = new SignalAccount(pair.first(), pair.second());
182
183 signalAccount.accountPath = accountPath;
184 signalAccount.number = number;
185 signalAccount.profileKey = profileKey;
186
187 signalAccount.dataPath = dataPath;
188 signalAccount.aciIdentityKeyPair = aciIdentityKey;
189 signalAccount.pniIdentityKeyPair = pniIdentityKey;
190 signalAccount.localRegistrationId = registrationId;
191 signalAccount.trustNewIdentity = trustNewIdentity;
192 signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, accountPath),
193 signalAccount.getRecipientStore(),
194 signalAccount::saveGroupStore);
195 signalAccount.stickerStore = new StickerStore(signalAccount::saveStickerStore);
196 signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
197
198 signalAccount.registered = false;
199
200 signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
201 signalAccount.migrateLegacyConfigs();
202 signalAccount.save();
203
204 return signalAccount;
205 }
206
207 public static SignalAccount createOrUpdateLinkedAccount(
208 File dataPath,
209 String accountPath,
210 String number,
211 ACI aci,
212 PNI pni,
213 String password,
214 String encryptedDeviceName,
215 int deviceId,
216 IdentityKeyPair aciIdentityKey,
217 IdentityKeyPair pniIdentityKey,
218 int registrationId,
219 ProfileKey profileKey,
220 final TrustNewIdentity trustNewIdentity
221 ) throws IOException {
222 IOUtils.createPrivateDirectories(dataPath);
223 var fileName = getFileName(dataPath, accountPath);
224 if (!fileName.exists()) {
225 return createLinkedAccount(dataPath,
226 accountPath,
227 number,
228 aci,
229 pni,
230 password,
231 encryptedDeviceName,
232 deviceId,
233 aciIdentityKey,
234 pniIdentityKey,
235 registrationId,
236 profileKey,
237 trustNewIdentity);
238 }
239
240 final var signalAccount = load(dataPath, accountPath, true, trustNewIdentity);
241 signalAccount.setProvisioningData(number,
242 aci,
243 pni,
244 password,
245 encryptedDeviceName,
246 deviceId,
247 aciIdentityKey,
248 pniIdentityKey,
249 profileKey);
250 signalAccount.getRecipientStore().resolveSelfRecipientTrusted(signalAccount.getSelfRecipientAddress());
251 signalAccount.getSessionStore().archiveAllSessions();
252 signalAccount.getSenderKeyStore().deleteAll();
253 signalAccount.clearAllPreKeys();
254 return signalAccount;
255 }
256
257 public void initDatabase() {
258 getAccountDatabase();
259 }
260
261 private void clearAllPreKeys() {
262 this.preKeyIdOffset = new SecureRandom().nextInt(Medium.MAX_VALUE);
263 this.nextSignedPreKeyId = new SecureRandom().nextInt(Medium.MAX_VALUE);
264 this.getPreKeyStore().removeAllPreKeys();
265 this.getSignedPreKeyStore().removeAllSignedPreKeys();
266 save();
267 }
268
269 private static SignalAccount createLinkedAccount(
270 File dataPath,
271 String accountPath,
272 String number,
273 ACI aci,
274 PNI pni,
275 String password,
276 String encryptedDeviceName,
277 int deviceId,
278 IdentityKeyPair aciIdentityKey,
279 IdentityKeyPair pniIdentityKey,
280 int registrationId,
281 ProfileKey profileKey,
282 final TrustNewIdentity trustNewIdentity
283 ) throws IOException {
284 var fileName = getFileName(dataPath, accountPath);
285 IOUtils.createPrivateFile(fileName);
286
287 final var pair = openFileChannel(fileName, true);
288 var signalAccount = new SignalAccount(pair.first(), pair.second());
289
290 signalAccount.setProvisioningData(number,
291 aci,
292 pni,
293 password,
294 encryptedDeviceName,
295 deviceId,
296 aciIdentityKey,
297 pniIdentityKey,
298 profileKey);
299
300 signalAccount.dataPath = dataPath;
301 signalAccount.accountPath = accountPath;
302 signalAccount.localRegistrationId = registrationId;
303 signalAccount.trustNewIdentity = trustNewIdentity;
304 signalAccount.groupStore = new GroupStore(getGroupCachePath(dataPath, accountPath),
305 signalAccount.getRecipientStore(),
306 signalAccount::saveGroupStore);
307 signalAccount.stickerStore = new StickerStore(signalAccount::saveStickerStore);
308 signalAccount.configurationStore = new ConfigurationStore(signalAccount::saveConfigurationStore);
309
310 signalAccount.getRecipientStore().resolveSelfRecipientTrusted(signalAccount.getSelfRecipientAddress());
311 signalAccount.previousStorageVersion = CURRENT_STORAGE_VERSION;
312 signalAccount.migrateLegacyConfigs();
313 signalAccount.clearAllPreKeys();
314 signalAccount.save();
315
316 return signalAccount;
317 }
318
319 private void setProvisioningData(
320 final String number,
321 final ACI aci,
322 final PNI pni,
323 final String password,
324 final String encryptedDeviceName,
325 final int deviceId,
326 final IdentityKeyPair aciIdentity,
327 final IdentityKeyPair pniIdentity,
328 final ProfileKey profileKey
329 ) {
330 this.number = number;
331 this.aci = aci;
332 this.pni = pni;
333 this.password = password;
334 this.profileKey = profileKey;
335 this.encryptedDeviceName = encryptedDeviceName;
336 this.deviceId = deviceId;
337 this.aciIdentityKeyPair = aciIdentity;
338 this.pniIdentityKeyPair = pniIdentity;
339 this.registered = true;
340 this.isMultiDevice = true;
341 this.lastReceiveTimestamp = 0;
342 this.pinMasterKey = null;
343 this.storageManifestVersion = -1;
344 this.storageKey = null;
345 }
346
347 private void migrateLegacyConfigs() {
348 if (getPassword() == null) {
349 setPassword(KeyUtils.createPassword());
350 }
351
352 if (getProfileKey() == null) {
353 // Old config file, creating new profile key
354 setProfileKey(KeyUtils.createProfileKey());
355 }
356 // Ensure our profile key is stored in profile store
357 getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey());
358 if (previousStorageVersion < 3) {
359 for (final var group : groupStore.getGroups()) {
360 if (group instanceof GroupInfoV2 && group.getDistributionId() == null) {
361 ((GroupInfoV2) group).setDistributionId(DistributionId.create());
362 groupStore.updateGroup(group);
363 }
364 }
365 save();
366 }
367 if (isMasterDevice() && getPniIdentityKeyPair() == null) {
368 setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
369 }
370 }
371
372 private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
373 getSessionStore().mergeRecipients(recipientId, toBeMergedRecipientId);
374 getIdentityKeyStore().mergeRecipients(recipientId, toBeMergedRecipientId);
375 getMessageCache().mergeRecipients(recipientId, toBeMergedRecipientId);
376 getGroupStore().mergeRecipients(recipientId, toBeMergedRecipientId);
377 getSenderKeyStore().mergeRecipients(recipientId, toBeMergedRecipientId);
378 }
379
380 public void removeRecipient(final RecipientId recipientId) {
381 getSessionStore().deleteAllSessions(recipientId);
382 getIdentityKeyStore().deleteIdentity(recipientId);
383 getMessageCache().deleteMessages(recipientId);
384 getSenderKeyStore().deleteAll(recipientId);
385 getRecipientStore().deleteRecipientData(recipientId);
386 }
387
388 public static File getFileName(File dataPath, String account) {
389 return new File(dataPath, account);
390 }
391
392 private static File getUserPath(final File dataPath, final String account) {
393 final var path = new File(dataPath, account + ".d");
394 try {
395 IOUtils.createPrivateDirectories(path);
396 } catch (IOException e) {
397 throw new AssertionError("Failed to create user path", e);
398 }
399 return path;
400 }
401
402 private static File getMessageCachePath(File dataPath, String account) {
403 return new File(getUserPath(dataPath, account), "msg-cache");
404 }
405
406 private static File getGroupCachePath(File dataPath, String account) {
407 return new File(getUserPath(dataPath, account), "group-cache");
408 }
409
410 private static File getPreKeysPath(File dataPath, String account) {
411 return new File(getUserPath(dataPath, account), "pre-keys");
412 }
413
414 private static File getSignedPreKeysPath(File dataPath, String account) {
415 return new File(getUserPath(dataPath, account), "signed-pre-keys");
416 }
417
418 private static File getIdentitiesPath(File dataPath, String account) {
419 return new File(getUserPath(dataPath, account), "identities");
420 }
421
422 private static File getSessionsPath(File dataPath, String account) {
423 return new File(getUserPath(dataPath, account), "sessions");
424 }
425
426 private static File getSenderKeysPath(File dataPath, String account) {
427 return new File(getUserPath(dataPath, account), "sender-keys");
428 }
429
430 private static File getSharedSenderKeysFile(File dataPath, String account) {
431 return new File(getUserPath(dataPath, account), "shared-sender-keys-store");
432 }
433
434 private static File getRecipientsStoreFile(File dataPath, String account) {
435 return new File(getUserPath(dataPath, account), "recipients-store");
436 }
437
438 private static File getDatabaseFile(File dataPath, String account) {
439 return new File(getUserPath(dataPath, account), "account.db");
440 }
441
442 public static boolean accountFileExists(File dataPath, String account) {
443 if (account == null) {
444 return false;
445 }
446 var f = getFileName(dataPath, account);
447 return !(!f.exists() || f.isDirectory());
448 }
449
450 private void load(
451 File dataPath, String accountPath, final TrustNewIdentity trustNewIdentity
452 ) throws IOException {
453 this.dataPath = dataPath;
454 this.accountPath = accountPath;
455 final JsonNode rootNode;
456 synchronized (fileChannel) {
457 fileChannel.position(0);
458 rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
459 }
460
461 if (rootNode.hasNonNull("version")) {
462 var accountVersion = rootNode.get("version").asInt(1);
463 if (accountVersion > CURRENT_STORAGE_VERSION) {
464 throw new IOException("Config file was created by a more recent version!");
465 } else if (accountVersion < MINIMUM_STORAGE_VERSION) {
466 throw new IOException("Config file was created by a no longer supported older version!");
467 }
468 previousStorageVersion = accountVersion;
469 }
470
471 number = Utils.getNotNullNode(rootNode, "username").asText();
472 if (rootNode.hasNonNull("password")) {
473 password = rootNode.get("password").asText();
474 }
475 registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
476 if (rootNode.hasNonNull("uuid")) {
477 try {
478 aci = ACI.parseOrThrow(rootNode.get("uuid").asText());
479 } catch (IllegalArgumentException e) {
480 throw new IOException("Config file contains an invalid aci/uuid, needs to be a valid UUID", e);
481 }
482 }
483 if (rootNode.hasNonNull("pni")) {
484 try {
485 pni = PNI.parseOrThrow(rootNode.get("pni").asText());
486 } catch (IllegalArgumentException e) {
487 throw new IOException("Config file contains an invalid pni, needs to be a valid UUID", e);
488 }
489 }
490 if (rootNode.hasNonNull("deviceName")) {
491 encryptedDeviceName = rootNode.get("deviceName").asText();
492 }
493 if (rootNode.hasNonNull("deviceId")) {
494 deviceId = rootNode.get("deviceId").asInt();
495 }
496 if (rootNode.hasNonNull("isMultiDevice")) {
497 isMultiDevice = rootNode.get("isMultiDevice").asBoolean();
498 }
499 if (rootNode.hasNonNull("lastReceiveTimestamp")) {
500 lastReceiveTimestamp = rootNode.get("lastReceiveTimestamp").asLong();
501 }
502 int registrationId = 0;
503 if (rootNode.hasNonNull("registrationId")) {
504 registrationId = rootNode.get("registrationId").asInt();
505 }
506 IdentityKeyPair aciIdentityKeyPair = null;
507 if (rootNode.hasNonNull("identityPrivateKey") && rootNode.hasNonNull("identityKey")) {
508 final var publicKeyBytes = Base64.getDecoder().decode(rootNode.get("identityKey").asText());
509 final var privateKeyBytes = Base64.getDecoder().decode(rootNode.get("identityPrivateKey").asText());
510 aciIdentityKeyPair = KeyUtils.getIdentityKeyPair(publicKeyBytes, privateKeyBytes);
511 }
512 if (rootNode.hasNonNull("pniIdentityPrivateKey") && rootNode.hasNonNull("pniIdentityKey")) {
513 final var publicKeyBytes = Base64.getDecoder().decode(rootNode.get("pniIdentityKey").asText());
514 final var privateKeyBytes = Base64.getDecoder().decode(rootNode.get("pniIdentityPrivateKey").asText());
515 pniIdentityKeyPair = KeyUtils.getIdentityKeyPair(publicKeyBytes, privateKeyBytes);
516 }
517
518 if (rootNode.hasNonNull("registrationLockPin")) {
519 registrationLockPin = rootNode.get("registrationLockPin").asText();
520 }
521 if (rootNode.hasNonNull("pinMasterKey")) {
522 pinMasterKey = new MasterKey(Base64.getDecoder().decode(rootNode.get("pinMasterKey").asText()));
523 }
524 if (rootNode.hasNonNull("storageKey")) {
525 storageKey = new StorageKey(Base64.getDecoder().decode(rootNode.get("storageKey").asText()));
526 }
527 if (rootNode.hasNonNull("storageManifestVersion")) {
528 storageManifestVersion = rootNode.get("storageManifestVersion").asLong();
529 }
530 if (rootNode.hasNonNull("preKeyIdOffset")) {
531 preKeyIdOffset = rootNode.get("preKeyIdOffset").asInt(1);
532 } else {
533 preKeyIdOffset = 1;
534 }
535 if (rootNode.hasNonNull("nextSignedPreKeyId")) {
536 nextSignedPreKeyId = rootNode.get("nextSignedPreKeyId").asInt(1);
537 } else {
538 nextSignedPreKeyId = 1;
539 }
540 if (rootNode.hasNonNull("profileKey")) {
541 try {
542 profileKey = new ProfileKey(Base64.getDecoder().decode(rootNode.get("profileKey").asText()));
543 } catch (InvalidInputException e) {
544 throw new IOException(
545 "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
546 e);
547 }
548 }
549
550 var migratedLegacyConfig = false;
551 final var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
552 ? jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
553 LegacyJsonSignalProtocolStore.class)
554 : null;
555 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
556 aciIdentityKeyPair = legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentityKeyPair();
557 registrationId = legacySignalProtocolStore.getLegacyIdentityKeyStore().getLocalRegistrationId();
558 migratedLegacyConfig = true;
559 }
560
561 this.aciIdentityKeyPair = aciIdentityKeyPair;
562 this.localRegistrationId = registrationId;
563 this.trustNewIdentity = trustNewIdentity;
564
565 migratedLegacyConfig = loadLegacyStores(rootNode, legacySignalProtocolStore) || migratedLegacyConfig;
566
567 if (rootNode.hasNonNull("groupStore")) {
568 groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class);
569 groupStore = GroupStore.fromStorage(groupStoreStorage,
570 getGroupCachePath(dataPath, accountPath),
571 getRecipientStore(),
572 this::saveGroupStore);
573 } else {
574 groupStore = new GroupStore(getGroupCachePath(dataPath, accountPath),
575 getRecipientStore(),
576 this::saveGroupStore);
577 }
578
579 if (rootNode.hasNonNull("stickerStore")) {
580 stickerStoreStorage = jsonProcessor.convertValue(rootNode.get("stickerStore"), StickerStore.Storage.class);
581 stickerStore = StickerStore.fromStorage(stickerStoreStorage, this::saveStickerStore);
582 } else {
583 stickerStore = new StickerStore(this::saveStickerStore);
584 }
585
586 if (rootNode.hasNonNull("configurationStore")) {
587 configurationStoreStorage = jsonProcessor.convertValue(rootNode.get("configurationStore"),
588 ConfigurationStore.Storage.class);
589 configurationStore = ConfigurationStore.fromStorage(configurationStoreStorage,
590 this::saveConfigurationStore);
591 } else {
592 configurationStore = new ConfigurationStore(this::saveConfigurationStore);
593 }
594
595 migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig;
596
597 if (migratedLegacyConfig) {
598 save();
599 }
600 }
601
602 private boolean loadLegacyStores(
603 final JsonNode rootNode, final LegacyJsonSignalProtocolStore legacySignalProtocolStore
604 ) {
605 var migrated = false;
606 var legacyRecipientStoreNode = rootNode.get("recipientStore");
607 if (legacyRecipientStoreNode != null) {
608 logger.debug("Migrating legacy recipient store.");
609 var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
610 if (legacyRecipientStore != null) {
611 getRecipientStore().resolveRecipientsTrusted(legacyRecipientStore.getAddresses());
612 }
613 getRecipientStore().resolveSelfRecipientTrusted(getSelfRecipientAddress());
614 migrated = true;
615 }
616
617 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
618 logger.debug("Migrating legacy pre key store.");
619 for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
620 try {
621 getPreKeyStore().storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
622 } catch (InvalidMessageException e) {
623 logger.warn("Failed to migrate pre key, ignoring", e);
624 }
625 }
626 migrated = true;
627 }
628
629 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
630 logger.debug("Migrating legacy signed pre key store.");
631 for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
632 try {
633 getSignedPreKeyStore().storeSignedPreKey(entry.getKey(), new SignedPreKeyRecord(entry.getValue()));
634 } catch (InvalidMessageException e) {
635 logger.warn("Failed to migrate signed pre key, ignoring", e);
636 }
637 }
638 migrated = true;
639 }
640
641 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySessionStore() != null) {
642 logger.debug("Migrating legacy session store.");
643 for (var session : legacySignalProtocolStore.getLegacySessionStore().getSessions()) {
644 try {
645 getSessionStore().storeSession(new SignalProtocolAddress(session.address.getIdentifier(),
646 session.deviceId), new SessionRecord(session.sessionRecord));
647 } catch (Exception e) {
648 logger.warn("Failed to migrate session, ignoring", e);
649 }
650 }
651 migrated = true;
652 }
653
654 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
655 logger.debug("Migrating legacy identity session store.");
656 for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
657 RecipientId recipientId = getRecipientStore().resolveRecipientTrusted(identity.getAddress());
658 getIdentityKeyStore().saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
659 getIdentityKeyStore().setIdentityTrustLevel(recipientId,
660 identity.getIdentityKey(),
661 identity.getTrustLevel());
662 }
663 migrated = true;
664 }
665
666 if (rootNode.hasNonNull("contactStore")) {
667 logger.debug("Migrating legacy contact store.");
668 final var contactStoreNode = rootNode.get("contactStore");
669 final var contactStore = jsonProcessor.convertValue(contactStoreNode, LegacyJsonContactsStore.class);
670 for (var contact : contactStore.getContacts()) {
671 final var recipientId = getRecipientStore().resolveRecipientTrusted(contact.getAddress());
672 getRecipientStore().storeContact(recipientId,
673 new Contact(contact.name,
674 contact.color,
675 contact.messageExpirationTime,
676 contact.blocked,
677 contact.archived));
678
679 // Store profile keys only in profile store
680 var profileKeyString = contact.profileKey;
681 if (profileKeyString != null) {
682 final ProfileKey profileKey;
683 try {
684 profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
685 getProfileStore().storeProfileKey(recipientId, profileKey);
686 } catch (InvalidInputException e) {
687 logger.warn("Failed to parse legacy contact profile key: {}", e.getMessage());
688 }
689 }
690 }
691 migrated = true;
692 }
693
694 if (rootNode.hasNonNull("profileStore")) {
695 logger.debug("Migrating legacy profile store.");
696 var profileStoreNode = rootNode.get("profileStore");
697 final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class);
698 for (var profileEntry : legacyProfileStore.getProfileEntries()) {
699 var recipientId = getRecipientStore().resolveRecipient(profileEntry.getAddress());
700 getRecipientStore().storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential());
701 getRecipientStore().storeProfileKey(recipientId, profileEntry.getProfileKey());
702 final var profile = profileEntry.getProfile();
703 if (profile != null) {
704 final var capabilities = new HashSet<Profile.Capability>();
705 if (profile.getCapabilities() != null) {
706 if (profile.getCapabilities().gv1Migration) {
707 capabilities.add(Profile.Capability.gv1Migration);
708 }
709 if (profile.getCapabilities().storage) {
710 capabilities.add(Profile.Capability.storage);
711 }
712 }
713 final var newProfile = new Profile(profileEntry.getLastUpdateTimestamp(),
714 profile.getGivenName(),
715 profile.getFamilyName(),
716 profile.getAbout(),
717 profile.getAboutEmoji(),
718 null,
719 profile.isUnrestrictedUnidentifiedAccess()
720 ? Profile.UnidentifiedAccessMode.UNRESTRICTED
721 : profile.getUnidentifiedAccess() != null
722 ? Profile.UnidentifiedAccessMode.ENABLED
723 : Profile.UnidentifiedAccessMode.DISABLED,
724 capabilities);
725 getRecipientStore().storeProfile(recipientId, newProfile);
726 }
727 }
728 }
729
730 return migrated;
731 }
732
733 private boolean loadLegacyThreadStore(final JsonNode rootNode) {
734 var threadStoreNode = rootNode.get("threadStore");
735 if (threadStoreNode != null && !threadStoreNode.isNull()) {
736 var threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
737 // Migrate thread info to group and contact store
738 for (var thread : threadStore.getThreads()) {
739 if (thread.id == null || thread.id.isEmpty()) {
740 continue;
741 }
742 try {
743 if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
744 final var recipientId = getRecipientStore().resolveRecipient(thread.id);
745 var contact = getRecipientStore().getContact(recipientId);
746 if (contact != null) {
747 getRecipientStore().storeContact(recipientId,
748 Contact.newBuilder(contact)
749 .withMessageExpirationTime(thread.messageExpirationTime)
750 .build());
751 }
752 } else {
753 var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
754 if (groupInfo instanceof GroupInfoV1) {
755 ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
756 groupStore.updateGroup(groupInfo);
757 }
758 }
759 } catch (Exception e) {
760 logger.warn("Failed to read legacy thread info: {}", e.getMessage());
761 }
762 }
763 return true;
764 }
765
766 return false;
767 }
768
769 private void saveStickerStore(StickerStore.Storage storage) {
770 this.stickerStoreStorage = storage;
771 save();
772 }
773
774 private void saveGroupStore(GroupStore.Storage storage) {
775 this.groupStoreStorage = storage;
776 save();
777 }
778
779 private void saveConfigurationStore(ConfigurationStore.Storage storage) {
780 this.configurationStoreStorage = storage;
781 save();
782 }
783
784 private void save() {
785 synchronized (fileChannel) {
786 var rootNode = jsonProcessor.createObjectNode();
787 rootNode.put("version", CURRENT_STORAGE_VERSION)
788 .put("username", number)
789 .put("uuid", aci == null ? null : aci.toString())
790 .put("pni", pni == null ? null : pni.toString())
791 .put("deviceName", encryptedDeviceName)
792 .put("deviceId", deviceId)
793 .put("isMultiDevice", isMultiDevice)
794 .put("lastReceiveTimestamp", lastReceiveTimestamp)
795 .put("password", password)
796 .put("registrationId", localRegistrationId)
797 .put("identityPrivateKey",
798 Base64.getEncoder().encodeToString(aciIdentityKeyPair.getPrivateKey().serialize()))
799 .put("identityKey",
800 Base64.getEncoder().encodeToString(aciIdentityKeyPair.getPublicKey().serialize()))
801 .put("pniIdentityPrivateKey",
802 pniIdentityKeyPair == null
803 ? null
804 : Base64.getEncoder()
805 .encodeToString(pniIdentityKeyPair.getPrivateKey().serialize()))
806 .put("pniIdentityKey",
807 pniIdentityKeyPair == null
808 ? null
809 : Base64.getEncoder().encodeToString(pniIdentityKeyPair.getPublicKey().serialize()))
810 .put("registrationLockPin", registrationLockPin)
811 .put("pinMasterKey",
812 pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
813 .put("storageKey",
814 storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
815 .put("storageManifestVersion", storageManifestVersion == -1 ? null : storageManifestVersion)
816 .put("preKeyIdOffset", preKeyIdOffset)
817 .put("nextSignedPreKeyId", nextSignedPreKeyId)
818 .put("profileKey",
819 profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
820 .put("registered", registered)
821 .putPOJO("groupStore", groupStoreStorage)
822 .putPOJO("stickerStore", stickerStoreStorage)
823 .putPOJO("configurationStore", configurationStoreStorage);
824 try {
825 try (var output = new ByteArrayOutputStream()) {
826 // Write to memory first to prevent corrupting the file in case of serialization errors
827 jsonProcessor.writeValue(output, rootNode);
828 var input = new ByteArrayInputStream(output.toByteArray());
829 fileChannel.position(0);
830 input.transferTo(Channels.newOutputStream(fileChannel));
831 fileChannel.truncate(fileChannel.position());
832 fileChannel.force(false);
833 }
834 } catch (Exception e) {
835 logger.error("Error saving file: {}", e.getMessage(), e);
836 }
837 }
838 }
839
840 private static Pair<FileChannel, FileLock> openFileChannel(File fileName, boolean waitForLock) throws IOException {
841 var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
842 var lock = fileChannel.tryLock();
843 if (lock == null) {
844 if (!waitForLock) {
845 logger.debug("Config file is in use by another instance.");
846 throw new IOException("Config file is in use by another instance.");
847 }
848 logger.info("Config file is in use by another instance, waiting…");
849 lock = fileChannel.lock();
850 logger.info("Config file lock acquired.");
851 }
852 return new Pair<>(fileChannel, lock);
853 }
854
855 public void addPreKeys(List<PreKeyRecord> records) {
856 for (var record : records) {
857 if (preKeyIdOffset != record.getId()) {
858 logger.error("Invalid pre key id {}, expected {}", record.getId(), preKeyIdOffset);
859 throw new AssertionError("Invalid pre key id");
860 }
861 getPreKeyStore().storePreKey(record.getId(), record);
862 preKeyIdOffset = (preKeyIdOffset + 1) % Medium.MAX_VALUE;
863 }
864 save();
865 }
866
867 public void addSignedPreKey(SignedPreKeyRecord record) {
868 if (nextSignedPreKeyId != record.getId()) {
869 logger.error("Invalid signed pre key id {}, expected {}", record.getId(), nextSignedPreKeyId);
870 throw new AssertionError("Invalid signed pre key id");
871 }
872 getSignedPreKeyStore().storeSignedPreKey(record.getId(), record);
873 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
874 save();
875 }
876
877 public SignalServiceDataStore getSignalServiceDataStore() {
878 return new SignalServiceDataStore() {
879 @Override
880 public SignalServiceAccountDataStore get(final ServiceId accountIdentifier) {
881 if (accountIdentifier.equals(aci)) {
882 return getSignalServiceAccountDataStore();
883 } else if (accountIdentifier.equals(pni)) {
884 throw new AssertionError("PNI not to be used yet!");
885 } else {
886 throw new IllegalArgumentException("No matching store found for " + accountIdentifier);
887 }
888 }
889
890 @Override
891 public SignalServiceAccountDataStore aci() {
892 return getSignalServiceAccountDataStore();
893 }
894
895 @Override
896 public SignalServiceAccountDataStore pni() {
897 throw new AssertionError("PNI not to be used yet!");
898 }
899
900 @Override
901 public boolean isMultiDevice() {
902 return SignalAccount.this.isMultiDevice();
903 }
904 };
905 }
906
907 public SignalServiceAccountDataStore getSignalServiceAccountDataStore() {
908 return getOrCreate(() -> signalProtocolStore,
909 () -> signalProtocolStore = new SignalProtocolStore(getPreKeyStore(),
910 getSignedPreKeyStore(),
911 getSessionStore(),
912 getIdentityKeyStore(),
913 getSenderKeyStore(),
914 this::isMultiDevice));
915 }
916
917 private PreKeyStore getPreKeyStore() {
918 return getOrCreate(() -> preKeyStore,
919 () -> preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, accountPath)));
920 }
921
922 private SignedPreKeyStore getSignedPreKeyStore() {
923 return getOrCreate(() -> signedPreKeyStore,
924 () -> signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, accountPath)));
925 }
926
927 public SessionStore getSessionStore() {
928 return getOrCreate(() -> sessionStore,
929 () -> sessionStore = new SessionStore(getSessionsPath(dataPath, accountPath), getRecipientStore()));
930 }
931
932 public IdentityKeyStore getIdentityKeyStore() {
933 return getOrCreate(() -> identityKeyStore,
934 () -> identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, accountPath),
935 getRecipientStore(),
936 aciIdentityKeyPair,
937 localRegistrationId,
938 trustNewIdentity));
939 }
940
941 public GroupStore getGroupStore() {
942 return groupStore;
943 }
944
945 public ContactsStore getContactStore() {
946 return getRecipientStore();
947 }
948
949 public RecipientStore getRecipientStore() {
950 return getOrCreate(() -> recipientStore,
951 () -> recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, accountPath),
952 this::mergeRecipients,
953 this::getSelfRecipientAddress));
954 }
955
956 public ProfileStore getProfileStore() {
957 return getRecipientStore();
958 }
959
960 public StickerStore getStickerStore() {
961 return stickerStore;
962 }
963
964 public SenderKeyStore getSenderKeyStore() {
965 return getOrCreate(() -> senderKeyStore,
966 () -> senderKeyStore = new SenderKeyStore(getSharedSenderKeysFile(dataPath, accountPath),
967 getSenderKeysPath(dataPath, accountPath),
968 getRecipientStore()::resolveRecipientAddress,
969 getRecipientStore()));
970 }
971
972 public ConfigurationStore getConfigurationStore() {
973 return configurationStore;
974 }
975
976 public MessageCache getMessageCache() {
977 return getOrCreate(() -> messageCache,
978 () -> messageCache = new MessageCache(getMessageCachePath(dataPath, accountPath)));
979 }
980
981 public AccountDatabase getAccountDatabase() {
982 return getOrCreate(() -> accountDatabase, () -> {
983 try {
984 accountDatabase = AccountDatabase.init(getDatabaseFile(dataPath, accountPath));
985 } catch (SQLException e) {
986 throw new RuntimeException(e);
987 }
988 });
989 }
990
991 public MessageSendLogStore getMessageSendLogStore() {
992 return getOrCreate(() -> messageSendLogStore,
993 () -> messageSendLogStore = new MessageSendLogStore(getRecipientStore(), getAccountDatabase()));
994 }
995
996 public CredentialsProvider getCredentialsProvider() {
997 return new CredentialsProvider() {
998 @Override
999 public ACI getAci() {
1000 return aci;
1001 }
1002
1003 @Override
1004 public PNI getPni() {
1005 return pni;
1006 }
1007
1008 @Override
1009 public String getE164() {
1010 return number;
1011 }
1012
1013 @Override
1014 public String getPassword() {
1015 return password;
1016 }
1017
1018 @Override
1019 public int getDeviceId() {
1020 return deviceId;
1021 }
1022 };
1023 }
1024
1025 public String getNumber() {
1026 return number;
1027 }
1028
1029 public void setNumber(final String number) {
1030 this.number = number;
1031 save();
1032 }
1033
1034 public ACI getAci() {
1035 return aci;
1036 }
1037
1038 public void setAci(final ACI aci) {
1039 this.aci = aci;
1040 save();
1041 }
1042
1043 public PNI getPni() {
1044 return pni;
1045 }
1046
1047 public void setPni(final PNI pni) {
1048 this.pni = pni;
1049 save();
1050 }
1051
1052 public SignalServiceAddress getSelfAddress() {
1053 return new SignalServiceAddress(aci, number);
1054 }
1055
1056 public RecipientAddress getSelfRecipientAddress() {
1057 return new RecipientAddress(aci == null ? null : aci.uuid(), number);
1058 }
1059
1060 public RecipientId getSelfRecipientId() {
1061 return getRecipientStore().resolveRecipient(getSelfRecipientAddress());
1062 }
1063
1064 public byte[] getEncryptedDeviceName() {
1065 return encryptedDeviceName == null ? null : Base64.getDecoder().decode(encryptedDeviceName);
1066 }
1067
1068 public void setEncryptedDeviceName(final String encryptedDeviceName) {
1069 this.encryptedDeviceName = encryptedDeviceName;
1070 save();
1071 }
1072
1073 public int getDeviceId() {
1074 return deviceId;
1075 }
1076
1077 public boolean isMasterDevice() {
1078 return deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID;
1079 }
1080
1081 public IdentityKeyPair getAciIdentityKeyPair() {
1082 return aciIdentityKeyPair;
1083 }
1084
1085 public IdentityKeyPair getPniIdentityKeyPair() {
1086 return pniIdentityKeyPair;
1087 }
1088
1089 public void setPniIdentityKeyPair(final IdentityKeyPair identityKeyPair) {
1090 pniIdentityKeyPair = identityKeyPair;
1091 save();
1092 }
1093
1094 public int getLocalRegistrationId() {
1095 return localRegistrationId;
1096 }
1097
1098 public String getPassword() {
1099 return password;
1100 }
1101
1102 private void setPassword(final String password) {
1103 this.password = password;
1104 save();
1105 }
1106
1107 public void setRegistrationLockPin(final String registrationLockPin, final MasterKey pinMasterKey) {
1108 this.registrationLockPin = registrationLockPin;
1109 this.pinMasterKey = pinMasterKey;
1110 save();
1111 }
1112
1113 public MasterKey getPinMasterKey() {
1114 return pinMasterKey;
1115 }
1116
1117 public StorageKey getStorageKey() {
1118 if (pinMasterKey != null) {
1119 return pinMasterKey.deriveStorageServiceKey();
1120 }
1121 return storageKey;
1122 }
1123
1124 public void setStorageKey(final StorageKey storageKey) {
1125 if (storageKey.equals(this.storageKey)) {
1126 return;
1127 }
1128 this.storageKey = storageKey;
1129 save();
1130 }
1131
1132 public long getStorageManifestVersion() {
1133 return this.storageManifestVersion;
1134 }
1135
1136 public void setStorageManifestVersion(final long storageManifestVersion) {
1137 if (storageManifestVersion == this.storageManifestVersion) {
1138 return;
1139 }
1140 this.storageManifestVersion = storageManifestVersion;
1141 save();
1142 }
1143
1144 public ProfileKey getProfileKey() {
1145 return profileKey;
1146 }
1147
1148 public void setProfileKey(final ProfileKey profileKey) {
1149 if (profileKey.equals(this.profileKey)) {
1150 return;
1151 }
1152 this.profileKey = profileKey;
1153 save();
1154 }
1155
1156 public byte[] getSelfUnidentifiedAccessKey() {
1157 return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
1158 }
1159
1160 public int getPreKeyIdOffset() {
1161 return preKeyIdOffset;
1162 }
1163
1164 public int getNextSignedPreKeyId() {
1165 return nextSignedPreKeyId;
1166 }
1167
1168 public boolean isRegistered() {
1169 return registered;
1170 }
1171
1172 public void setRegistered(final boolean registered) {
1173 this.registered = registered;
1174 save();
1175 }
1176
1177 public boolean isMultiDevice() {
1178 return isMultiDevice;
1179 }
1180
1181 public void setMultiDevice(final boolean multiDevice) {
1182 if (isMultiDevice == multiDevice) {
1183 return;
1184 }
1185 isMultiDevice = multiDevice;
1186 save();
1187 }
1188
1189 public long getLastReceiveTimestamp() {
1190 return lastReceiveTimestamp;
1191 }
1192
1193 public void setLastReceiveTimestamp(final long lastReceiveTimestamp) {
1194 this.lastReceiveTimestamp = lastReceiveTimestamp;
1195 save();
1196 }
1197
1198 public boolean isUnrestrictedUnidentifiedAccess() {
1199 // TODO make configurable
1200 return false;
1201 }
1202
1203 public boolean isDiscoverableByPhoneNumber() {
1204 return configurationStore.getPhoneNumberUnlisted() == null || !configurationStore.getPhoneNumberUnlisted();
1205 }
1206
1207 public void finishRegistration(final ACI aci, final PNI pni, final MasterKey masterKey, final String pin) {
1208 this.pinMasterKey = masterKey;
1209 this.storageManifestVersion = -1;
1210 this.storageKey = null;
1211 this.encryptedDeviceName = null;
1212 this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
1213 this.isMultiDevice = false;
1214 this.registered = true;
1215 this.aci = aci;
1216 this.pni = pni;
1217 this.registrationLockPin = pin;
1218 this.lastReceiveTimestamp = 0;
1219 save();
1220
1221 clearAllPreKeys();
1222 getSessionStore().archiveAllSessions();
1223 getSenderKeyStore().deleteAll();
1224 final var recipientId = getRecipientStore().resolveSelfRecipientTrusted(getSelfRecipientAddress());
1225 final var publicKey = getAciIdentityKeyPair().getPublicKey();
1226 getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
1227 getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
1228 }
1229
1230 @Override
1231 public void close() {
1232 synchronized (fileChannel) {
1233 if (accountDatabase != null) {
1234 try {
1235 accountDatabase.close();
1236 } catch (SQLException e) {
1237 logger.warn("Failed to close account database: {}", e.getMessage(), e);
1238 }
1239 }
1240 if (messageSendLogStore != null) {
1241 messageSendLogStore.close();
1242 }
1243 try {
1244 try {
1245 lock.close();
1246 } catch (ClosedChannelException ignored) {
1247 }
1248 fileChannel.close();
1249 } catch (IOException e) {
1250 logger.warn("Failed to close account: {}", e.getMessage(), e);
1251 }
1252 }
1253 }
1254
1255 private <T> T getOrCreate(Supplier<T> supplier, Callable creator) {
1256 var value = supplier.get();
1257 if (value != null) {
1258 return value;
1259 }
1260
1261 synchronized (LOCK) {
1262 value = supplier.get();
1263 if (value != null) {
1264 return value;
1265 }
1266 creator.call();
1267 return supplier.get();
1268 }
1269 }
1270
1271 private interface Callable {
1272
1273 void call();
1274 }
1275 }