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