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