]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
6671bc76e4a96943271af3601b3afc65bebb49c9
[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",
581 profileKey == null ? null : Base64.getEncoder().encodeToString(profileKey.serialize()))
582 .put("registered", registered)
583 .putPOJO("groupStore", groupStoreStorage)
584 .putPOJO("stickerStore", stickerStoreStorage);
585 try {
586 try (var output = new ByteArrayOutputStream()) {
587 // Write to memory first to prevent corrupting the file in case of serialization errors
588 jsonProcessor.writeValue(output, rootNode);
589 var input = new ByteArrayInputStream(output.toByteArray());
590 fileChannel.position(0);
591 input.transferTo(Channels.newOutputStream(fileChannel));
592 fileChannel.truncate(fileChannel.position());
593 fileChannel.force(false);
594 }
595 } catch (Exception e) {
596 logger.error("Error saving file: {}", e.getMessage());
597 }
598 }
599 }
600
601 private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
602 var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
603 var lock = fileChannel.tryLock();
604 if (lock == null) {
605 logger.info("Config file is in use by another instance, waiting…");
606 lock = fileChannel.lock();
607 logger.info("Config file lock acquired.");
608 }
609 return new Pair<>(fileChannel, lock);
610 }
611
612 public void addPreKeys(List<PreKeyRecord> records) {
613 for (var record : records) {
614 if (preKeyIdOffset != record.getId()) {
615 logger.error("Invalid pre key id {}, expected {}", record.getId(), preKeyIdOffset);
616 throw new AssertionError("Invalid pre key id");
617 }
618 preKeyStore.storePreKey(record.getId(), record);
619 preKeyIdOffset = (preKeyIdOffset + 1) % Medium.MAX_VALUE;
620 }
621 save();
622 }
623
624 public void addSignedPreKey(SignedPreKeyRecord record) {
625 if (nextSignedPreKeyId != record.getId()) {
626 logger.error("Invalid signed pre key id {}, expected {}", record.getId(), nextSignedPreKeyId);
627 throw new AssertionError("Invalid signed pre key id");
628 }
629 signalProtocolStore.storeSignedPreKey(record.getId(), record);
630 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
631 save();
632 }
633
634 public SignalProtocolStore getSignalProtocolStore() {
635 return signalProtocolStore;
636 }
637
638 public SessionStore getSessionStore() {
639 return sessionStore;
640 }
641
642 public IdentityKeyStore getIdentityKeyStore() {
643 return identityKeyStore;
644 }
645
646 public GroupStore getGroupStore() {
647 return groupStore;
648 }
649
650 public ContactsStore getContactStore() {
651 return recipientStore;
652 }
653
654 public RecipientStore getRecipientStore() {
655 return recipientStore;
656 }
657
658 public ProfileStore getProfileStore() {
659 return recipientStore;
660 }
661
662 public StickerStore getStickerStore() {
663 return stickerStore;
664 }
665
666 public MessageCache getMessageCache() {
667 return messageCache;
668 }
669
670 public String getUsername() {
671 return username;
672 }
673
674 public UUID getUuid() {
675 return uuid;
676 }
677
678 public void setUuid(final UUID uuid) {
679 this.uuid = uuid;
680 save();
681 }
682
683 public SignalServiceAddress getSelfAddress() {
684 return new SignalServiceAddress(uuid, username);
685 }
686
687 public RecipientId getSelfRecipientId() {
688 return recipientStore.resolveRecipientTrusted(getSelfAddress());
689 }
690
691 public int getDeviceId() {
692 return deviceId;
693 }
694
695 public boolean isMasterDevice() {
696 return deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID;
697 }
698
699 public IdentityKeyPair getIdentityKeyPair() {
700 return signalProtocolStore.getIdentityKeyPair();
701 }
702
703 public int getLocalRegistrationId() {
704 return signalProtocolStore.getLocalRegistrationId();
705 }
706
707 public String getPassword() {
708 return password;
709 }
710
711 private void setPassword(final String password) {
712 this.password = password;
713 save();
714 }
715
716 public String getRegistrationLockPin() {
717 return registrationLockPin;
718 }
719
720 public void setRegistrationLockPin(final String registrationLockPin, final MasterKey pinMasterKey) {
721 this.registrationLockPin = registrationLockPin;
722 this.pinMasterKey = pinMasterKey;
723 save();
724 }
725
726 public MasterKey getPinMasterKey() {
727 return pinMasterKey;
728 }
729
730 public StorageKey getStorageKey() {
731 if (pinMasterKey != null) {
732 return pinMasterKey.deriveStorageServiceKey();
733 }
734 return storageKey;
735 }
736
737 public void setStorageKey(final StorageKey storageKey) {
738 if (storageKey.equals(this.storageKey)) {
739 return;
740 }
741 this.storageKey = storageKey;
742 save();
743 }
744
745 public ProfileKey getProfileKey() {
746 return profileKey;
747 }
748
749 public void setProfileKey(final ProfileKey profileKey) {
750 if (profileKey.equals(this.profileKey)) {
751 return;
752 }
753 this.profileKey = profileKey;
754 save();
755 }
756
757 public byte[] getSelfUnidentifiedAccessKey() {
758 return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
759 }
760
761 public int getPreKeyIdOffset() {
762 return preKeyIdOffset;
763 }
764
765 public int getNextSignedPreKeyId() {
766 return nextSignedPreKeyId;
767 }
768
769 public boolean isRegistered() {
770 return registered;
771 }
772
773 public void setRegistered(final boolean registered) {
774 this.registered = registered;
775 save();
776 }
777
778 public boolean isMultiDevice() {
779 return isMultiDevice;
780 }
781
782 public void setMultiDevice(final boolean multiDevice) {
783 if (isMultiDevice == multiDevice) {
784 return;
785 }
786 isMultiDevice = multiDevice;
787 save();
788 }
789
790 public boolean isUnrestrictedUnidentifiedAccess() {
791 // TODO make configurable
792 return false;
793 }
794
795 public boolean isDiscoverableByPhoneNumber() {
796 // TODO make configurable
797 return true;
798 }
799
800 public void finishRegistration(final UUID uuid, final MasterKey masterKey, final String pin) {
801 this.pinMasterKey = masterKey;
802 this.deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
803 this.isMultiDevice = false;
804 this.registered = true;
805 this.uuid = uuid;
806 this.registrationLockPin = pin;
807 save();
808
809 getSessionStore().archiveAllSessions();
810 final var recipientId = getRecipientStore().resolveRecipientTrusted(getSelfAddress());
811 final var publicKey = getIdentityKeyPair().getPublicKey();
812 getIdentityKeyStore().saveIdentity(recipientId, publicKey, new Date());
813 getIdentityKeyStore().setIdentityTrustLevel(recipientId, publicKey, TrustLevel.TRUSTED_VERIFIED);
814 }
815
816 @Override
817 public void close() throws IOException {
818 synchronized (fileChannel) {
819 try {
820 lock.close();
821 } catch (ClosedChannelException ignored) {
822 }
823 fileChannel.close();
824 }
825 }
826 }