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