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