]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
ea3b5ea85400491f83af7dbd9a0db94cd9b61585
[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 username = Utils.getNotNullNode(rootNode, "username").asText();
286 password = Utils.getNotNullNode(rootNode, "password").asText();
287 registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
288 if (rootNode.hasNonNull("uuid")) {
289 try {
290 uuid = UUID.fromString(rootNode.get("uuid").asText());
291 } catch (IllegalArgumentException e) {
292 throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e);
293 }
294 }
295 if (rootNode.hasNonNull("deviceId")) {
296 deviceId = rootNode.get("deviceId").asInt();
297 }
298 if (rootNode.hasNonNull("isMultiDevice")) {
299 isMultiDevice = rootNode.get("isMultiDevice").asBoolean();
300 }
301 int registrationId = 0;
302 if (rootNode.hasNonNull("registrationId")) {
303 registrationId = rootNode.get("registrationId").asInt();
304 }
305 IdentityKeyPair identityKeyPair = null;
306 if (rootNode.hasNonNull("identityPrivateKey") && rootNode.hasNonNull("identityKey")) {
307 final var publicKeyBytes = Base64.getDecoder().decode(rootNode.get("identityKey").asText());
308 final var privateKeyBytes = Base64.getDecoder().decode(rootNode.get("identityPrivateKey").asText());
309 identityKeyPair = KeyUtils.getIdentityKeyPair(publicKeyBytes, privateKeyBytes);
310 }
311
312 if (rootNode.hasNonNull("registrationLockPin")) {
313 registrationLockPin = rootNode.get("registrationLockPin").asText();
314 }
315 if (rootNode.hasNonNull("pinMasterKey")) {
316 pinMasterKey = new MasterKey(Base64.getDecoder().decode(rootNode.get("pinMasterKey").asText()));
317 }
318 if (rootNode.hasNonNull("storageKey")) {
319 storageKey = new StorageKey(Base64.getDecoder().decode(rootNode.get("storageKey").asText()));
320 }
321 if (rootNode.hasNonNull("preKeyIdOffset")) {
322 preKeyIdOffset = rootNode.get("preKeyIdOffset").asInt(0);
323 } else {
324 preKeyIdOffset = 0;
325 }
326 if (rootNode.hasNonNull("nextSignedPreKeyId")) {
327 nextSignedPreKeyId = rootNode.get("nextSignedPreKeyId").asInt();
328 } else {
329 nextSignedPreKeyId = 0;
330 }
331 if (rootNode.hasNonNull("profileKey")) {
332 try {
333 profileKey = new ProfileKey(Base64.getDecoder().decode(rootNode.get("profileKey").asText()));
334 } catch (InvalidInputException e) {
335 throw new IOException(
336 "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
337 e);
338 }
339 }
340
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 }
349
350 recipientStore = RecipientStore.load(getRecipientsStoreFile(dataPath, username), this::mergeRecipients);
351
352 preKeyStore = new PreKeyStore(getPreKeysPath(dataPath, username));
353 signedPreKeyStore = new SignedPreKeyStore(getSignedPreKeysPath(dataPath, username));
354 sessionStore = new SessionStore(getSessionsPath(dataPath, username), recipientStore::resolveRecipient);
355 identityKeyStore = new IdentityKeyStore(getIdentitiesPath(dataPath, username),
356 recipientStore::resolveRecipient,
357 identityKeyPair,
358 registrationId);
359
360 loadLegacyStores(rootNode, legacySignalProtocolStore);
361
362 signalProtocolStore = new SignalProtocolStore(preKeyStore, signedPreKeyStore, sessionStore, identityKeyStore);
363
364 var groupStoreNode = rootNode.get("groupStore");
365 if (groupStoreNode != null) {
366 groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
367 groupStore.groupCachePath = getGroupCachePath(dataPath, username);
368 }
369 if (groupStore == null) {
370 groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
371 }
372
373 var stickerStoreNode = rootNode.get("stickerStore");
374 if (stickerStoreNode != null) {
375 stickerStore = jsonProcessor.convertValue(stickerStoreNode, StickerStore.class);
376 }
377 if (stickerStore == null) {
378 stickerStore = new StickerStore();
379 }
380
381 messageCache = new MessageCache(getMessageCachePath(dataPath, username));
382
383 loadLegacyThreadStore(rootNode);
384 }
385
386 private void loadLegacyStores(
387 final JsonNode rootNode, final LegacyJsonSignalProtocolStore legacySignalProtocolStore
388 ) {
389 var legacyRecipientStoreNode = rootNode.get("recipientStore");
390 if (legacyRecipientStoreNode != null) {
391 logger.debug("Migrating legacy recipient store.");
392 var legacyRecipientStore = jsonProcessor.convertValue(legacyRecipientStoreNode, LegacyRecipientStore.class);
393 if (legacyRecipientStore != null) {
394 recipientStore.resolveRecipientsTrusted(legacyRecipientStore.getAddresses());
395 }
396 recipientStore.resolveRecipientTrusted(getSelfAddress());
397 }
398
399 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacyPreKeyStore() != null) {
400 logger.debug("Migrating legacy pre key store.");
401 for (var entry : legacySignalProtocolStore.getLegacyPreKeyStore().getPreKeys().entrySet()) {
402 try {
403 preKeyStore.storePreKey(entry.getKey(), new PreKeyRecord(entry.getValue()));
404 } catch (IOException e) {
405 logger.warn("Failed to migrate pre key, ignoring", e);
406 }
407 }
408 }
409
410 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySignedPreKeyStore() != null) {
411 logger.debug("Migrating legacy signed pre key store.");
412 for (var entry : legacySignalProtocolStore.getLegacySignedPreKeyStore().getSignedPreKeys().entrySet()) {
413 try {
414 signedPreKeyStore.storeSignedPreKey(entry.getKey(), new SignedPreKeyRecord(entry.getValue()));
415 } catch (IOException e) {
416 logger.warn("Failed to migrate signed pre key, ignoring", e);
417 }
418 }
419 }
420
421 if (legacySignalProtocolStore != null && legacySignalProtocolStore.getLegacySessionStore() != null) {
422 logger.debug("Migrating legacy session store.");
423 for (var session : legacySignalProtocolStore.getLegacySessionStore().getSessions()) {
424 try {
425 sessionStore.storeSession(new SignalProtocolAddress(session.address.getIdentifier(),
426 session.deviceId), new SessionRecord(session.sessionRecord));
427 } catch (IOException e) {
428 logger.warn("Failed to migrate session, ignoring", e);
429 }
430 }
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 }
443
444 if (rootNode.hasNonNull("contactStore")) {
445 logger.debug("Migrating legacy contact store.");
446 final var contactStoreNode = rootNode.get("contactStore");
447 final var contactStore = jsonProcessor.convertValue(contactStoreNode, LegacyJsonContactsStore.class);
448 for (var contact : contactStore.getContacts()) {
449 final var recipientId = recipientStore.resolveRecipientTrusted(contact.getAddress());
450 recipientStore.storeContact(recipientId,
451 new Contact(contact.name,
452 contact.color,
453 contact.messageExpirationTime,
454 contact.blocked,
455 contact.archived));
456
457 // Store profile keys only in profile store
458 var profileKeyString = contact.profileKey;
459 if (profileKeyString != null) {
460 final ProfileKey profileKey;
461 try {
462 profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
463 getProfileStore().storeProfileKey(recipientId, profileKey);
464 } catch (InvalidInputException e) {
465 logger.warn("Failed to parse legacy contact profile key: {}", e.getMessage());
466 }
467 }
468 }
469 }
470
471 if (rootNode.hasNonNull("profileStore")) {
472 logger.debug("Migrating legacy profile store.");
473 var profileStoreNode = rootNode.get("profileStore");
474 final var legacyProfileStore = jsonProcessor.convertValue(profileStoreNode, LegacyProfileStore.class);
475 for (var profileEntry : legacyProfileStore.getProfileEntries()) {
476 var recipientId = recipientStore.resolveRecipient(profileEntry.getServiceAddress());
477 recipientStore.storeProfileKey(recipientId, profileEntry.getProfileKey());
478 recipientStore.storeProfileKeyCredential(recipientId, profileEntry.getProfileKeyCredential());
479 final var profile = profileEntry.getProfile();
480 if (profile != null) {
481 final var capabilities = new HashSet<Profile.Capability>();
482 if (profile.getCapabilities().gv1Migration) {
483 capabilities.add(Profile.Capability.gv1Migration);
484 }
485 if (profile.getCapabilities().gv2) {
486 capabilities.add(Profile.Capability.gv2);
487 }
488 if (profile.getCapabilities().storage) {
489 capabilities.add(Profile.Capability.storage);
490 }
491 final var newProfile = new Profile(profileEntry.getLastUpdateTimestamp(),
492 profile.getGivenName(),
493 profile.getFamilyName(),
494 profile.getAbout(),
495 profile.getAboutEmoji(),
496 profile.isUnrestrictedUnidentifiedAccess()
497 ? Profile.UnidentifiedAccessMode.UNRESTRICTED
498 : profile.getUnidentifiedAccess() != null
499 ? Profile.UnidentifiedAccessMode.ENABLED
500 : Profile.UnidentifiedAccessMode.DISABLED,
501 capabilities);
502 recipientStore.storeProfile(recipientId, newProfile);
503 }
504 }
505 }
506 }
507
508 private void loadLegacyThreadStore(final JsonNode rootNode) {
509 var threadStoreNode = rootNode.get("threadStore");
510 if (threadStoreNode != null && !threadStoreNode.isNull()) {
511 var threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
512 // Migrate thread info to group and contact store
513 for (var thread : threadStore.getThreads()) {
514 if (thread.id == null || thread.id.isEmpty()) {
515 continue;
516 }
517 try {
518 if (UuidUtil.isUuid(thread.id) || thread.id.startsWith("+")) {
519 final var recipientId = recipientStore.resolveRecipient(thread.id);
520 var contact = recipientStore.getContact(recipientId);
521 if (contact != null) {
522 recipientStore.storeContact(recipientId,
523 Contact.newBuilder(contact)
524 .withMessageExpirationTime(thread.messageExpirationTime)
525 .build());
526 }
527 } else {
528 var groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
529 if (groupInfo instanceof GroupInfoV1) {
530 ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
531 groupStore.updateGroup(groupInfo);
532 }
533 }
534 } catch (Exception e) {
535 logger.warn("Failed to read legacy thread info: {}", e.getMessage());
536 }
537 }
538 }
539 }
540
541 public void save() {
542 if (fileChannel == null) {
543 return;
544 }
545 var rootNode = jsonProcessor.createObjectNode();
546 rootNode.put("username", username)
547 .put("uuid", uuid == null ? null : uuid.toString())
548 .put("deviceId", deviceId)
549 .put("isMultiDevice", isMultiDevice)
550 .put("password", password)
551 .put("registrationId", identityKeyStore.getLocalRegistrationId())
552 .put("identityPrivateKey",
553 Base64.getEncoder()
554 .encodeToString(identityKeyStore.getIdentityKeyPair().getPrivateKey().serialize()))
555 .put("identityKey",
556 Base64.getEncoder()
557 .encodeToString(identityKeyStore.getIdentityKeyPair().getPublicKey().serialize()))
558 .put("registrationLockPin", registrationLockPin)
559 .put("pinMasterKey",
560 pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
561 .put("storageKey",
562 storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
563 .put("preKeyIdOffset", preKeyIdOffset)
564 .put("nextSignedPreKeyId", nextSignedPreKeyId)
565 .put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
566 .put("registered", registered)
567 .putPOJO("groupStore", groupStore)
568 .putPOJO("stickerStore", stickerStore);
569 try {
570 try (var output = new ByteArrayOutputStream()) {
571 // Write to memory first to prevent corrupting the file in case of serialization errors
572 jsonProcessor.writeValue(output, rootNode);
573 var input = new ByteArrayInputStream(output.toByteArray());
574 synchronized (fileChannel) {
575 fileChannel.position(0);
576 input.transferTo(Channels.newOutputStream(fileChannel));
577 fileChannel.truncate(fileChannel.position());
578 fileChannel.force(false);
579 }
580 }
581 } catch (Exception e) {
582 logger.error("Error saving file: {}", e.getMessage());
583 }
584 }
585
586 private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
587 var fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
588 var lock = fileChannel.tryLock();
589 if (lock == null) {
590 logger.info("Config file is in use by another instance, waiting…");
591 lock = fileChannel.lock();
592 logger.info("Config file lock acquired.");
593 }
594 return new Pair<>(fileChannel, lock);
595 }
596
597 public void addPreKeys(List<PreKeyRecord> records) {
598 for (var record : records) {
599 if (preKeyIdOffset != record.getId()) {
600 logger.error("Invalid pre key id {}, expected {}", record.getId(), preKeyIdOffset);
601 throw new AssertionError("Invalid pre key id");
602 }
603 preKeyStore.storePreKey(record.getId(), record);
604 preKeyIdOffset = (preKeyIdOffset + 1) % Medium.MAX_VALUE;
605 }
606 save();
607 }
608
609 public void addSignedPreKey(SignedPreKeyRecord record) {
610 if (nextSignedPreKeyId != record.getId()) {
611 logger.error("Invalid signed pre key id {}, expected {}", record.getId(), nextSignedPreKeyId);
612 throw new AssertionError("Invalid signed pre key id");
613 }
614 signalProtocolStore.storeSignedPreKey(record.getId(), record);
615 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
616 save();
617 }
618
619 public SignalProtocolStore getSignalProtocolStore() {
620 return signalProtocolStore;
621 }
622
623 public SessionStore getSessionStore() {
624 return sessionStore;
625 }
626
627 public IdentityKeyStore getIdentityKeyStore() {
628 return identityKeyStore;
629 }
630
631 public JsonGroupStore getGroupStore() {
632 return groupStore;
633 }
634
635 public ContactsStore getContactStore() {
636 return recipientStore;
637 }
638
639 public RecipientStore getRecipientStore() {
640 return recipientStore;
641 }
642
643 public ProfileStore getProfileStore() {
644 return recipientStore;
645 }
646
647 public StickerStore getStickerStore() {
648 return stickerStore;
649 }
650
651 public MessageCache getMessageCache() {
652 return messageCache;
653 }
654
655 public String getUsername() {
656 return username;
657 }
658
659 public UUID getUuid() {
660 return uuid;
661 }
662
663 public void setUuid(final UUID uuid) {
664 this.uuid = uuid;
665 }
666
667 public SignalServiceAddress getSelfAddress() {
668 return new SignalServiceAddress(uuid, username);
669 }
670
671 public RecipientId getSelfRecipientId() {
672 return recipientStore.resolveRecipientTrusted(getSelfAddress());
673 }
674
675 public int getDeviceId() {
676 return deviceId;
677 }
678
679 public void setDeviceId(final int deviceId) {
680 this.deviceId = deviceId;
681 }
682
683 public boolean isMasterDevice() {
684 return deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID;
685 }
686
687 public IdentityKeyPair getIdentityKeyPair() {
688 return signalProtocolStore.getIdentityKeyPair();
689 }
690
691 public int getLocalRegistrationId() {
692 return signalProtocolStore.getLocalRegistrationId();
693 }
694
695 public String getPassword() {
696 return password;
697 }
698
699 public void setPassword(final String password) {
700 this.password = password;
701 }
702
703 public String getRegistrationLockPin() {
704 return registrationLockPin;
705 }
706
707 public void setRegistrationLockPin(final String registrationLockPin) {
708 this.registrationLockPin = registrationLockPin;
709 }
710
711 public MasterKey getPinMasterKey() {
712 return pinMasterKey;
713 }
714
715 public void setPinMasterKey(final MasterKey pinMasterKey) {
716 this.pinMasterKey = pinMasterKey;
717 }
718
719 public StorageKey getStorageKey() {
720 if (pinMasterKey != null) {
721 return pinMasterKey.deriveStorageServiceKey();
722 }
723 return storageKey;
724 }
725
726 public void setStorageKey(final StorageKey storageKey) {
727 this.storageKey = storageKey;
728 }
729
730 public ProfileKey getProfileKey() {
731 return profileKey;
732 }
733
734 public void setProfileKey(final ProfileKey profileKey) {
735 this.profileKey = profileKey;
736 }
737
738 public byte[] getSelfUnidentifiedAccessKey() {
739 return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
740 }
741
742 public int getPreKeyIdOffset() {
743 return preKeyIdOffset;
744 }
745
746 public int getNextSignedPreKeyId() {
747 return nextSignedPreKeyId;
748 }
749
750 public boolean isRegistered() {
751 return registered;
752 }
753
754 public void setRegistered(final boolean registered) {
755 this.registered = registered;
756 }
757
758 public boolean isMultiDevice() {
759 return isMultiDevice;
760 }
761
762 public void setMultiDevice(final boolean multiDevice) {
763 isMultiDevice = multiDevice;
764 }
765
766 public boolean isUnrestrictedUnidentifiedAccess() {
767 // TODO make configurable
768 return false;
769 }
770
771 public boolean isDiscoverableByPhoneNumber() {
772 // TODO make configurable
773 return true;
774 }
775
776 @Override
777 public void close() throws IOException {
778 if (fileChannel.isOpen()) {
779 save();
780 }
781 synchronized (fileChannel) {
782 try {
783 lock.close();
784 } catch (ClosedChannelException ignored) {
785 }
786 fileChannel.close();
787 }
788 }
789 }