]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
6d920cf6ed1b86cb0edba7cc92b06ca73fbd295d
[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.TrustLevel;
7 import org.asamk.signal.manager.groups.GroupId;
8 import org.asamk.signal.manager.storage.contacts.ContactsStore;
9 import org.asamk.signal.manager.storage.contacts.LegacyJsonContactsStore;
10 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
11 import org.asamk.signal.manager.storage.groups.GroupStore;
12 import org.asamk.signal.manager.storage.identities.IdentityKeyStore;
13 import org.asamk.signal.manager.storage.messageCache.MessageCache;
14 import org.asamk.signal.manager.storage.prekeys.PreKeyStore;
15 import org.asamk.signal.manager.storage.prekeys.SignedPreKeyStore;
16 import org.asamk.signal.manager.storage.profiles.LegacyProfileStore;
17 import org.asamk.signal.manager.storage.profiles.ProfileStore;
18 import org.asamk.signal.manager.storage.protocol.LegacyJsonSignalProtocolStore;
19 import org.asamk.signal.manager.storage.protocol.SignalProtocolStore;
20 import org.asamk.signal.manager.storage.recipients.Contact;
21 import org.asamk.signal.manager.storage.recipients.LegacyRecipientStore;
22 import org.asamk.signal.manager.storage.recipients.Profile;
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.signal.zkgroup.InvalidInputException;
31 import org.signal.zkgroup.profiles.ProfileKey;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34 import org.whispersystems.libsignal.IdentityKeyPair;
35 import org.whispersystems.libsignal.SignalProtocolAddress;
36 import org.whispersystems.libsignal.state.PreKeyRecord;
37 import org.whispersystems.libsignal.state.SessionRecord;
38 import org.whispersystems.libsignal.state.SignedPreKeyRecord;
39 import org.whispersystems.libsignal.util.Medium;
40 import org.whispersystems.libsignal.util.Pair;
41 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
42 import org.whispersystems.signalservice.api.kbs.MasterKey;
43 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
44 import org.whispersystems.signalservice.api.storage.StorageKey;
45 import org.whispersystems.signalservice.api.util.UuidUtil;
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.Date;
59 import java.util.HashSet;
60 import java.util.List;
61 import java.util.UUID;
62
63 public class SignalAccount implements Closeable {
64
65 private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
66
67 private final ObjectMapper jsonProcessor = Utils.createStorageObjectMapper();
68
69 private final FileChannel fileChannel;
70 private final FileLock lock;
71
72 private String username;
73 private UUID uuid;
74 private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
75 private boolean isMultiDevice = false;
76 private String password;
77 private String registrationLockPin;
78 private MasterKey pinMasterKey;
79 private StorageKey storageKey;
80 private ProfileKey profileKey;
81 private int preKeyIdOffset;
82 private int nextSignedPreKeyId;
83
84 private boolean registered = false;
85
86 private SignalProtocolStore signalProtocolStore;
87 private PreKeyStore preKeyStore;
88 private SignedPreKeyStore signedPreKeyStore;
89 private SessionStore sessionStore;
90 private IdentityKeyStore identityKeyStore;
91 private GroupStore groupStore;
92 private GroupStore.Storage groupStoreStorage;
93 private RecipientStore recipientStore;
94 private StickerStore stickerStore;
95 private StickerStore.Storage stickerStoreStorage;
96
97 private MessageCache messageCache;
98
99 private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
100 this.fileChannel = fileChannel;
101 this.lock = lock;
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
135 account.initStores(dataPath, identityKey, registrationId);
136 account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
137 account.recipientStore::resolveRecipient,
138 account::saveGroupStore);
139 account.stickerStore = new StickerStore(account::saveStickerStore);
140
141 account.registered = false;
142
143 account.migrateLegacyConfigs();
144 account.save();
145
146 return account;
147 }
148
149 private void initStores(
150 final File dataPath, final IdentityKeyPair identityKey, final int registrationId
151 ) throws IOException {
152 recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
153
154 preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
155 signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
156 sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient);
157 identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
158 recipientStore::resolveRecipient,
159 identityKey,
160 registrationId);
161 signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore);
162
163 messageCache = new MessageCache(getMessageCachePath(dataPath, username));
164 }
165
166 public static SignalAccount createLinkedAccount(
167 File dataPath,
168 String username,
169 UUID uuid,
170 String password,
171 int deviceId,
172 IdentityKeyPair identityKey,
173 int registrationId,
174 ProfileKey profileKey
175 ) throws IOException {
176 IOUtils.createPrivateDirectories(dataPath);
177 var fileName = getFileName(dataPath, username);
178 if (!fileName.exists()) {
179 IOUtils.createPrivateFile(fileName);
180 }
181
182 final var pair = openFileChannel(fileName);
183 var account = new SignalAccount(pair.first(), pair.second());
184
185 account.username = username;
186 account.uuid = uuid;
187 account.password = password;
188 account.profileKey = profileKey;
189 account.deviceId = deviceId;
190
191 account.initStores(dataPath, identityKey, registrationId);
192 account.groupStore = new GroupStore(getGroupCachePath(dataPath, username),
193 account.recipientStore::resolveRecipient,
194 account::saveGroupStore);
195 account.stickerStore = new StickerStore(account::saveStickerStore);
196
197 account.registered = true;
198 account.isMultiDevice = true;
199
200 account.recipientStore.resolveRecipientTrusted(account.getSelfAddress());
201 account.migrateLegacyConfigs();
202 account.save();
203
204 return account;
205 }
206
207 private void migrateLegacyConfigs() {
208 if (getPassword() == null) {
209 setPassword(KeyUtils.createPassword());
210 }
211
212 if (getProfileKey() == null && isRegistered()) {
213 // Old config file, creating new profile key
214 setProfileKey(KeyUtils.createProfileKey());
215 }
216 // Ensure our profile key is stored in profile store
217 getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey());
218 }
219
220 private void mergeRecipients(RecipientId recipientId, RecipientId toBeMergedRecipientId) {
221 sessionStore.mergeRecipients(recipientId, toBeMergedRecipientId);
222 identityKeyStore.mergeRecipients(recipientId, toBeMergedRecipientId);
223 messageCache.mergeRecipients(recipientId, toBeMergedRecipientId);
224 groupStore.mergeRecipients(recipientId, toBeMergedRecipientId);
225 }
226
227 public static File getFileName(File dataPath, String username) {
228 return new File(dataPath, username);
229 }
230
231 private static File getUserPath(final File dataPath, final String username) {
232 final var path = new File(dataPath, username + ".d");
233 try {
234 IOUtils.createPrivateDirectories(path);
235 } catch (IOException e) {
236 throw new AssertionError("Failed to create user path", e);
237 }
238 return path;
239 }
240
241 private static File getMessageCachePath(File dataPath, String username) {
242 return new File(getUserPath(dataPath, username), "msg-cache");
243 }
244
245 private static File getGroupCachePath(File dataPath, String username) {
246 return new File(getUserPath(dataPath, username), "group-cache");
247 }
248
249 private static File getPreKeysPath(File dataPath, String username) {
250 return new File(getUserPath(dataPath, username), "pre-keys");
251 }
252
253 private static File getSignedPreKeysPath(File dataPath, String username) {
254 return new File(getUserPath(dataPath, username), "signed-pre-keys");
255 }
256
257 private static File getIdentitiesPath(File dataPath, String username) {
258 return new File(getUserPath(dataPath, username), "identities");
259 }
260
261 private static File getSessionsPath(File dataPath, String username) {
262 return new File(getUserPath(dataPath, username), "sessions");
263 }
264
265 private static File getRecipientsStoreFile(File dataPath, String username) {
266 return new File(getUserPath(dataPath, username), "recipients-store");
267 }
268
269 public static boolean userExists(File dataPath, String username) {
270 if (username == null) {
271 return false;
272 }
273 var f = getFileName(dataPath, username);
274 return !(!f.exists() || f.isDirectory());
275 }
276
277 private void load(File dataPath) throws IOException {
278 JsonNode rootNode;
279 synchronized (fileChannel) {
280 fileChannel.position(0);
281 rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
282 }
283
284 username = Utils.getNotNullNode(rootNode, "username").asText();
285 password = Utils.getNotNullNode(rootNode, "password").asText();
286 registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
287 if (rootNode.hasNonNull("uuid")) {
288 try {
289 uuid = UUID.fromString(rootNode.get("uuid").asText());
290 } catch (IllegalArgumentException e) {
291 throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e);
292 }
293 }
294 if (rootNode.hasNonNull("deviceId")) {
295 deviceId = rootNode.get("deviceId").asInt();
296 }
297 if (rootNode.hasNonNull("isMultiDevice")) {
298 isMultiDevice = rootNode.get("isMultiDevice").asBoolean();
299 }
300 int registrationId = 0;
301 if (rootNode.hasNonNull("registrationId")) {
302 registrationId = rootNode.get("registrationId").asInt();
303 }
304 IdentityKeyPair identityKeyPair = null;
305 if (rootNode.hasNonNull("identityPrivateKey") && rootNode.hasNonNull("identityKey")) {
306 final var publicKeyBytes = Base64.getDecoder().decode(rootNode.get("identityKey").asText());
307 final var privateKeyBytes = Base64.getDecoder().decode(rootNode.get("identityPrivateKey").asText());
308 identityKeyPair = KeyUtils.getIdentityKeyPair(publicKeyBytes, privateKeyBytes);
309 }
310
311 if (rootNode.hasNonNull("registrationLockPin")) {
312 registrationLockPin = rootNode.get("registrationLockPin").asText();
313 }
314 if (rootNode.hasNonNull("pinMasterKey")) {
315 pinMasterKey = new MasterKey(Base64.getDecoder().decode(rootNode.get("pinMasterKey").asText()));
316 }
317 if (rootNode.hasNonNull("storageKey")) {
318 storageKey = new StorageKey(Base64.getDecoder().decode(rootNode.get("storageKey").asText()));
319 }
320 if (rootNode.hasNonNull("preKeyIdOffset")) {
321 preKeyIdOffset = rootNode.get("preKeyIdOffset").asInt(0);
322 } else {
323 preKeyIdOffset = 0;
324 }
325 if (rootNode.hasNonNull("nextSignedPreKeyId")) {
326 nextSignedPreKeyId = rootNode.get("nextSignedPreKeyId").asInt();
327 } else {
328 nextSignedPreKeyId = 0;
329 }
330 if (rootNode.hasNonNull("profileKey")) {
331 try {
332 profileKey = new ProfileKey(Base64.getDecoder().decode(rootNode.get("profileKey").asText()));
333 } catch (InvalidInputException e) {
334 throw new IOException(
335 "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
336 e);
337 }
338 }
339
340 var migratedLegacyConfig = false;
341 final var legacySignalProtocolStore = rootNode.hasNonNull("axolotlStore")
342 ? jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
343 LegacyJsonSignalProtocolStore.class)
344 : null;
345 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
346 identityKeyPair = legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentityKeyPair();
347 registrationId = legacySignalProtocolStore.getLegacyIdentityKeyStore().getLocalRegistrationId();
348 migratedLegacyConfig = true;
349 }
350
351 initStores(dataPath, identityKeyPair, registrationId);
352
353 migratedLegacyConfig = loadLegacyStores(rootNode, legacySignalProtocolStore) || migratedLegacyConfig;
354
355 if (rootNode.hasNonNull("groupStore")) {
356 groupStoreStorage = jsonProcessor.convertValue(rootNode.get("groupStore"), GroupStore.Storage.class);
357 groupStore = GroupStore.fromStorage(groupStoreStorage,
358 getGroupCachePath(dataPath, username),
359 recipientStore::resolveRecipient,
360 this::saveGroupStore);
361 } else {
362 groupStore = new GroupStore(getGroupCachePath(dataPath, username),
363 recipientStore::resolveRecipient,
364 this::saveGroupStore);
365 }
366
367 if (rootNode.hasNonNull("stickerStore")) {
368 stickerStoreStorage = jsonProcessor.convertValue(rootNode.get("stickerStore"), StickerStore.Storage.class);
369 stickerStore = StickerStore.fromStorage(stickerStoreStorage, this::saveStickerStore);
370 } else {
371 stickerStore = new StickerStore(this::saveStickerStore);
372 }
373
374 migratedLegacyConfig = loadLegacyThreadStore(rootNode) || migratedLegacyConfig;
375
376 if (migratedLegacyConfig) {
377 save();
378 }
379 }
380
381 private boolean loadLegacyStores(
382 final JsonNode rootNode, final LegacyJsonSignalProtocolStore legacySignalProtocolStore
383 ) {
384 var migrated = false;
385 var legacyRecipientStoreNode = rootNode.get("recipientStore");
386 if (legacyRecipientStoreNode != null) {
387 logger.debug("Migrating legacy recipient store.");
388 var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
389 if (legacyRecipientStore != null) {
390 recipientStore.resolveRecipientsTrusted(legacyRecipientStore.getAddresses());
391 }
392 recipientStore.resolveRecipientTrusted(getSelfAddress());
393 migrated = true;
394 }
395
396 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
397 logger.debug("Migrating legacy pre key store.");
398 for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
399 try {
400 preKeyStore.storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
401 } catch (IOException e) {
402 logger.warn("Failed to migrate pre key, ignoring", e);
403 }
404 }
405 migrated = true;
406 }
407
408 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
409 logger.debug("Migrating legacy signed pre key store.");
410 for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
411 try {
412 signedPreKeyStore.storeSignedPreKey(entry.getKey(), new SignedPreKeyRecord(entry.getValue()));
413 } catch (IOException e) {
414 logger.warn("Failed to migrate signed pre key, ignoring", e);
415 }
416 }
417 migrated = true;
418 }
419
420 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySessionStore() != null) {
421 logger.debug("Migrating legacy session store.");
422 for (var session : legacySignalProtocolStore.getLegacySessionStore().getSessions()) {
423 try {
424 sessionStore.storeSession(new SignalProtocolAddress(session.address.getIdentifier(),
425 session.deviceId), new SessionRecord(session.sessionRecord));
426 } catch (IOException e) {
427 logger.warn("Failed to migrate session, ignoring", e);
428 }
429 }
430 migrated = true;
431 }
432
433 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyIdentityKeyStore() != null) {
434 logger.debug("Migrating legacy identity session store.");
435 for (var identity : legacySignalProtocolStore.getLegacyIdentityKeyStore().getIdentities()) {
436 RecipientId recipientId = recipientStore.resolveRecipientTrusted(identity.getAddress());
437 identityKeyStore.saveIdentity(recipientId, identity.getIdentityKey(), identity.getDateAdded());
438 identityKeyStore.setIdentityTrustLevel(recipientId,
439 identity.getIdentityKey(),
440 identity.getTrustLevel());
441 }
442 migrated = true;
443 }
444
445 if (rootNode.hasNonNull("contactStore")) {
446 logger.debug("Migrating legacy contact store.");
447 final var contactStoreNode = rootNode.get("contactStore");
448 final var contactStore = jsonProcessor.convertValue(contactStoreNode, LegacyJsonContactsStore.class);
449 for (var contact : contactStore.getContacts()) {
450 final var recipientId = recipientStore.resolveRecipientTrusted(contact.getAddress());
451 recipientStore.storeContact(recipientId,
452 new Contact(contact.name,
453 contact.color,
454 contact.messageExpirationTime,
455 contact.blocked,
456 contact.archived));
457
458 // Store profile keys only in profile store
459 var profileKeyString = contact.profileKey;
460 if (profileKeyString != null) {
461 final ProfileKey profileKey;
462 try {
463 profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
464 getProfileStore().storeProfileKey(recipientId, profileKey);
465 } catch (InvalidInputException e) {
466 logger.warn("Failed to parse legacy contact profile key: {}", e.getMessage());
467 }
468 }
469 }
470 migrated = true;
471 }
472
473 if (rootNode.hasNonNull("profileStore")) {
474 logger.debug("Migrating legacy profile store.");
475 var profileStoreNode = rootNode.get("profileStore");
476 final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class);
477 for (var profileEntry : legacyProfileStore.getProfileEntries()) {
478 var recipientId = recipientStore.resolveRecipient(profileEntry.getServiceAddress());
479 recipientStore.storeProfileKey(recipientId, profileEntry.getProfileKey());
480 recipientStore.storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential());
481 final var profile = profileEntry.getProfile();
482 if (profile != null) {
483 final var capabilities = new HashSet<Profile.Capability>();
484 if (profile.getCapabilities().gv1Migration) {
485 capabilities.add(Profile.Capability.gv1Migration);
486 }
487 if (profile.getCapabilities().gv2) {
488 capabilities.add(Profile.Capability.gv2);
489 }
490 if (profile.getCapabilities().storage) {
491 capabilities.add(Profile.Capability.storage);
492 }
493 final var newProfile = new Profile(profileEntry.getLastUpdateTimestamp(),
494 profile.getGivenName(),
495 profile.getFamilyName(),
496 profile.getAbout(),
497 profile.getAboutEmoji(),
498 profile.isUnrestrictedUnidentifiedAccess()
499 ? Profile.UnidentifiedAccessMode.UNRESTRICTED
500 : profile.getUnidentifiedAccess() != null
501 ? Profile.UnidentifiedAccessMode.ENABLED
502 : Profile.UnidentifiedAccessMode.DISABLED,
503 capabilities);
504 recipientStore.storeProfile(recipientId, newProfile);
505 }
506 }
507 }
508
509 return migrated;
510 }
511
512 private boolean loadLegacyThreadStore(final JsonNode rootNode) {
513 var threadStoreNode = rootNode.get("threadStore");
514 if (threadStoreNode != null && !threadStoreNode.isNull()) {
515 var threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
516 // Migrate thread info to group and contact store
517 for (var thread : threadStore.getThreads()) {
518 if (thread.id == null || thread.id.isEmpty()) {
519 continue;
520 }
521 try {
522 if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
523 final var recipientId = recipientStore.resolveRecipient(thread.id);
524 var contact = recipientStore.getContact(recipientId);
525 if (contact != null) {
526 recipientStore.storeContact(recipientId,
527 Contact.newBuilder(contact)
528 .withMessageExpirationTime(thread.messageExpirationTime)
529 .build());
530 }
531 } else {
532 var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
533 if (groupInfo instanceof GroupInfoV1) {
534 ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
535 groupStore.updateGroup(groupInfo);
536 }
537 }
538 } catch (Exception e) {
539 logger.warn("Failed to read legacy thread info: {}", e.getMessage());
540 }
541 }
542 return true;
543 }
544
545 return false;
546 }
547
548 private void saveStickerStore(StickerStore.Storage storage) {
549 this.stickerStoreStorage = storage;
550 save();
551 }
552
553 private void saveGroupStore(GroupStore.Storage storage) {
554 this.groupStoreStorage = storage;
555 save();
556 }
557
558 private void save() {
559 synchronized (fileChannel) {
560 var rootNode = jsonProcessor.createObjectNode();
561 rootNode.put("username", username)
562 .put("uuid", uuid == null ? null : uuid.toString())
563 .put("deviceId", deviceId)
564 .put("isMultiDevice", isMultiDevice)
565 .put("password", password)
566 .put("registrationId", identityKeyStore.getLocalRegistrationId())
567 .put("identityPrivateKey",
568 Base64.getEncoder()
569 .encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
570 .put("identityKey",
571 Base64.getEncoder()
572 .encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
573 .put("registrationLockPin", registrationLockPin)
574 .put("pinMasterKey",
575 pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
576 .put("storageKey",
577 storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
578 .put("preKeyIdOffset", preKeyIdOffset)
579 .put("nextSignedPreKeyId", nextSignedPreKeyId)
580 .put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
581 .put("registered", registered)
582 .putPOJO("groupStore", groupStoreStorage)
583 .putPOJO("stickerStore", stickerStoreStorage);
584 try {
585 try (var output = new ByteArrayOutputStream()) {
586 // Write to memory first to prevent corrupting the file in case of serialization errors
587 jsonProcessor.writeValue(output, rootNode);
588 var input = new ByteArrayInputStream(output.toByteArray());
589 fileChannel.position(0);
590 input.transferTo(Channels.newOutputStream(fileChannel));
591 fileChannel.truncate(fileChannel.position());
592 fileChannel.force(false);
593 }
594 } catch (Exception e) {
595 logger.error("Error saving file: {}", e.getMessage());
596 }
597 }
598 }
599
600 private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
601 var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
602 var lock = fileChannel.tryLock();
603 if (lock == null) {
604 logger.info("Config file is in use by another instance, waiting…");
605 lock = fileChannel.lock();
606 logger.info("Config file lock acquired.");
607 }
608 return new Pair<>(fileChannel, lock);
609 }
610
611 public void addPreKeys(List<PreKeyRecord> records) {
612 for (var record : records) {
613 if (preKeyIdOffset != record.getId()) {
614 logger.error("Invalid pre key id {}, expected {}", record.getId(), preKeyIdOffset);
615 throw new AssertionError("Invalid pre key id");
616 }
617 preKeyStore.storePreKey(record.getId(), record);
618 preKeyIdOffset = (preKeyIdOffset + 1) % Medium.MAX_VALUE;
619 }
620 save();
621 }
622
623 public void addSignedPreKey(SignedPreKeyRecord record) {
624 if (nextSignedPreKeyId != record.getId()) {
625 logger.error("Invalid signed pre key id {}, expected {}", record.getId(), nextSignedPreKeyId);
626 throw new AssertionError("Invalid signed pre key id");
627 }
628 signalProtocolStore.storeSignedPreKey(record.getId(), record);
629 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
630 save();
631 }
632
633 public SignalProtocolStore getSignalProtocolStore() {
634 return signalProtocolStore;
635 }
636
637 public SessionStore getSessionStore() {
638 return sessionStore;
639 }
640
641 public IdentityKeyStore getIdentityKeyStore() {
642 return identityKeyStore;
643 }
644
645 public GroupStore getGroupStore() {
646 return groupStore;
647 }
648
649 public ContactsStore getContactStore() {
650 return recipientStore;
651 }
652
653 public RecipientStore getRecipientStore() {
654 return recipientStore;
655 }
656
657 public ProfileStore getProfileStore() {
658 return recipientStore;
659 }
660
661 public StickerStore getStickerStore() {
662 return stickerStore;
663 }
664
665 public MessageCache getMessageCache() {
666 return messageCache;
667 }
668
669 public String getUsername() {
670 return username;
671 }
672
673 public UUID getUuid() {
674 return uuid;
675 }
676
677 public void setUuid(final UUID uuid) {
678 this.uuid = uuid;
679 save();
680 }
681
682 public SignalServiceAddress getSelfAddress() {
683 return new SignalServiceAddress(uuid, username);
684 }
685
686 public RecipientId getSelfRecipientId() {
687 return recipientStore.resolveRecipientTrusted(getSelfAddress());
688 }
689
690 public int getDeviceId() {
691 return deviceId;
692 }
693
694 public boolean isMasterDevice() {
695 return deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID;
696 }
697
698 public IdentityKeyPair getIdentityKeyPair() {
699 return signalProtocolStore.getIdentityKeyPair();
700 }
701
702 public int getLocalRegistrationId() {
703 return signalProtocolStore.getLocalRegistrationId();
704 }
705
706 public String getPassword() {
707 return password;
708 }
709
710 private void setPassword(final String password) {
711 this.password = password;
712 save();
713 }
714
715 public String getRegistrationLockPin() {
716 return registrationLockPin;
717 }
718
719 public void setRegistrationLockPin(final String registrationLockPin, final MasterKey pinMasterKey) {
720 this.registrationLockPin = registrationLockPin;
721 this.pinMasterKey = pinMasterKey;
722 save();
723 }
724
725 public MasterKey getPinMasterKey() {
726 return pinMasterKey;
727 }
728
729 public StorageKey getStorageKey() {
730 if (pinMasterKey != null) {
731 return pinMasterKey.deriveStorageServiceKey();
732 }
733 return storageKey;
734 }
735
736 public void setStorageKey(final StorageKey storageKey) {
737 if (storageKey.equals(this.storageKey)) {
738 return;
739 }
740 this.storageKey = storageKey;
741 save();
742 }
743
744 public ProfileKey getProfileKey() {
745 return profileKey;
746 }
747
748 public void setProfileKey(final ProfileKey profileKey) {
749 if (profileKey.equals(this.profileKey)) {
750 return;
751 }
752 this.profileKey = profileKey;
753 save();
754 }
755
756 public byte[] getSelfUnidentifiedAccessKey() {
757 return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
758 }
759
760 public int getPreKeyIdOffset() {
761 return preKeyIdOffset;
762 }
763
764 public int getNextSignedPreKeyId() {
765 return nextSignedPreKeyId;
766 }
767
768 public boolean isRegistered() {
769 return registered;
770 }
771
772 public void setRegistered(final boolean registered) {
773 this.registered = registered;
774 save();
775 }
776
777 public boolean isMultiDevice() {
778 return isMultiDevice;
779 }
780
781 public void setMultiDevice(final boolean multiDevice) {
782 if (isMultiDevice == multiDevice) {
783 return;
784 }
785 isMultiDevice = multiDevice;
786 save();
787 }
788
789 public boolean isUnrestrictedUnidentifiedAccess() {
790 // TODO make configurable
791 return false;
792 }
793
794 public boolean isDiscoverableByPhoneNumber() {
795 // TODO make configurable
796 return true;
797 }
798
799 public void finishRegistration(final UUID uuid, final MasterKey masterKey, final String pin) {
800 this.pinMasterKey = masterKey;
801 this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
802 this.isMultiDevice = false;
803 this.registered = true;
804 this.uuid = uuid;
805 this.registrationLockPin = pin;
806 save();
807
808 getSessionStore().archiveAllSessions();
809 final var recipientId = getRecipientStore().resolveRecipientTrusted(getSelfAddress());
810 final var publicKey = getIdentityKeyPair().getPublicKey();
811 getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
812 getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
813 }
814
815 @Override
816 public void close() throws IOException {
817 synchronized (fileChannel) {
818 try {
819 lock.close();
820 } catch (ClosedChannelException ignored) {
821 }
822 fileChannel.close();
823 }
824 }
825 }