]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
78e30db80850211917d0aabc7fe93c33f025612d
[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 import com.fasterxml.jackson.databind.node.ObjectNode;
12
13 import org.asamk.signal.manager.groups.GroupId;
14 import org.asamk.signal.manager.storage.contacts.ContactInfo;
15 import org.asamk.signal.manager.storage.contacts.JsonContactsStore;
16 import org.asamk.signal.manager.storage.groups.GroupInfo;
17 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
18 import org.asamk.signal.manager.storage.groups.JsonGroupStore;
19 import org.asamk.signal.manager.storage.messageCache.MessageCache;
20 import org.asamk.signal.manager.storage.profiles.ProfileStore;
21 import org.asamk.signal.manager.storage.protocol.IdentityInfo;
22 import org.asamk.signal.manager.storage.protocol.JsonSignalProtocolStore;
23 import org.asamk.signal.manager.storage.protocol.RecipientStore;
24 import org.asamk.signal.manager.storage.protocol.SessionInfo;
25 import org.asamk.signal.manager.storage.protocol.SignalServiceAddressResolver;
26 import org.asamk.signal.manager.storage.stickers.StickerStore;
27 import org.asamk.signal.manager.storage.threads.LegacyJsonThreadStore;
28 import org.asamk.signal.manager.storage.threads.ThreadInfo;
29 import org.asamk.signal.manager.util.IOUtils;
30 import org.asamk.signal.manager.util.KeyUtils;
31 import org.asamk.signal.manager.util.Utils;
32 import org.signal.zkgroup.InvalidInputException;
33 import org.signal.zkgroup.profiles.ProfileKey;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36 import org.whispersystems.libsignal.IdentityKeyPair;
37 import org.whispersystems.libsignal.state.PreKeyRecord;
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
46 import java.io.ByteArrayInputStream;
47 import java.io.ByteArrayOutputStream;
48 import java.io.Closeable;
49 import java.io.File;
50 import java.io.IOException;
51 import java.io.RandomAccessFile;
52 import java.nio.channels.Channels;
53 import java.nio.channels.ClosedChannelException;
54 import java.nio.channels.FileChannel;
55 import java.nio.channels.FileLock;
56 import java.util.Base64;
57 import java.util.Collection;
58 import java.util.UUID;
59 import java.util.stream.Collectors;
60
61 public class SignalAccount implements Closeable {
62
63 private final static Logger logger = LoggerFactory.getLogger(SignalAccount.class);
64
65 private final ObjectMapper jsonProcessor = new ObjectMapper();
66 private final FileChannel fileChannel;
67 private final FileLock lock;
68 private String username;
69 private UUID uuid;
70 private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
71 private boolean isMultiDevice = false;
72 private String password;
73 private String registrationLockPin;
74 private MasterKey pinMasterKey;
75 private StorageKey storageKey;
76 private String signalingKey;
77 private ProfileKey profileKey;
78 private int preKeyIdOffset;
79 private int nextSignedPreKeyId;
80
81 private boolean registered = false;
82
83 private JsonSignalProtocolStore signalProtocolStore;
84 private JsonGroupStore groupStore;
85 private JsonContactsStore contactStore;
86 private RecipientStore recipientStore;
87 private ProfileStore profileStore;
88 private StickerStore stickerStore;
89
90 private MessageCache messageCache;
91
92 private SignalAccount(final FileChannel fileChannel, final FileLock lock) {
93 this.fileChannel = fileChannel;
94 this.lock = lock;
95 jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
96 jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print
97 jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
98 jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
99 jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
100 }
101
102 public static SignalAccount load(File dataPath, String username) throws IOException {
103 final File fileName = getFileName(dataPath, username);
104 final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
105 try {
106 SignalAccount account = new SignalAccount(pair.first(), pair.second());
107 account.load(dataPath);
108 account.migrateLegacyConfigs();
109
110 return account;
111 } catch (Throwable e) {
112 pair.second().close();
113 pair.first().close();
114 throw e;
115 }
116 }
117
118 public static SignalAccount create(
119 File dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey
120 ) throws IOException {
121 IOUtils.createPrivateDirectories(dataPath);
122 File fileName = getFileName(dataPath, username);
123 if (!fileName.exists()) {
124 IOUtils.createPrivateFile(fileName);
125 }
126
127 final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
128 SignalAccount account = new SignalAccount(pair.first(), pair.second());
129
130 account.username = username;
131 account.profileKey = profileKey;
132 account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
133 account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
134 account.contactStore = new JsonContactsStore();
135 account.recipientStore = new RecipientStore();
136 account.profileStore = new ProfileStore();
137 account.stickerStore = new StickerStore();
138
139 account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
140
141 account.registered = false;
142
143 account.migrateLegacyConfigs();
144
145 return account;
146 }
147
148 public static SignalAccount createLinkedAccount(
149 File dataPath,
150 String username,
151 UUID uuid,
152 String password,
153 int deviceId,
154 IdentityKeyPair identityKey,
155 int registrationId,
156 String signalingKey,
157 ProfileKey profileKey
158 ) throws IOException {
159 IOUtils.createPrivateDirectories(dataPath);
160 File fileName = getFileName(dataPath, username);
161 if (!fileName.exists()) {
162 IOUtils.createPrivateFile(fileName);
163 }
164
165 final Pair<FileChannel, FileLock> pair = openFileChannel(fileName);
166 SignalAccount account = new SignalAccount(pair.first(), pair.second());
167
168 account.username = username;
169 account.uuid = uuid;
170 account.password = password;
171 account.profileKey = profileKey;
172 account.deviceId = deviceId;
173 account.signalingKey = signalingKey;
174 account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
175 account.groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
176 account.contactStore = new JsonContactsStore();
177 account.recipientStore = new RecipientStore();
178 account.profileStore = new ProfileStore();
179 account.stickerStore = new StickerStore();
180
181 account.messageCache = new MessageCache(getMessageCachePath(dataPath, username));
182
183 account.registered = true;
184 account.isMultiDevice = true;
185
186 account.migrateLegacyConfigs();
187
188 return account;
189 }
190
191 public void migrateLegacyConfigs() {
192 if (getProfileKey() == null && isRegistered()) {
193 // Old config file, creating new profile key
194 setProfileKey(KeyUtils.createProfileKey());
195 save();
196 }
197 // Store profile keys only in profile store
198 for (ContactInfo contact : getContactStore().getContacts()) {
199 String profileKeyString = contact.profileKey;
200 if (profileKeyString == null) {
201 continue;
202 }
203 final ProfileKey profileKey;
204 try {
205 profileKey = new ProfileKey(Base64.getDecoder().decode(profileKeyString));
206 } catch (InvalidInputException ignored) {
207 continue;
208 }
209 contact.profileKey = null;
210 getProfileStore().storeProfileKey(contact.getAddress(), profileKey);
211 }
212 // Ensure our profile key is stored in profile store
213 getProfileStore().storeProfileKey(getSelfAddress(), getProfileKey());
214 }
215
216 public static File getFileName(File dataPath, String username) {
217 return new File(dataPath, username);
218 }
219
220 private static File getUserPath(final File dataPath, final String username) {
221 return new File(dataPath, username + ".d");
222 }
223
224 public static File getMessageCachePath(File dataPath, String username) {
225 return new File(getUserPath(dataPath, username), "msg-cache");
226 }
227
228 private static File getGroupCachePath(File dataPath, String username) {
229 return new File(getUserPath(dataPath, username), "group-cache");
230 }
231
232 public static boolean userExists(File dataPath, String username) {
233 if (username == null) {
234 return false;
235 }
236 File f = getFileName(dataPath, username);
237 return !(!f.exists() || f.isDirectory());
238 }
239
240 private void load(File dataPath) throws IOException {
241 JsonNode rootNode;
242 synchronized (fileChannel) {
243 fileChannel.position(0);
244 rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
245 }
246
247 if (rootNode.hasNonNull("uuid")) {
248 try {
249 uuid = UUID.fromString(rootNode.get("uuid").asText());
250 } catch (IllegalArgumentException e) {
251 throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e);
252 }
253 }
254 if (rootNode.hasNonNull("deviceId")) {
255 deviceId = rootNode.get("deviceId").asInt();
256 }
257 if (rootNode.hasNonNull("isMultiDevice")) {
258 isMultiDevice = rootNode.get("isMultiDevice").asBoolean();
259 }
260 username = Utils.getNotNullNode(rootNode, "username").asText();
261 password = Utils.getNotNullNode(rootNode, "password").asText();
262 if (rootNode.hasNonNull("registrationLockPin")) {
263 registrationLockPin = rootNode.get("registrationLockPin").asText();
264 }
265 if (rootNode.hasNonNull("pinMasterKey")) {
266 pinMasterKey = new MasterKey(Base64.getDecoder().decode(rootNode.get("pinMasterKey").asText()));
267 }
268 if (rootNode.hasNonNull("storageKey")) {
269 storageKey = new StorageKey(Base64.getDecoder().decode(rootNode.get("storageKey").asText()));
270 }
271 if (rootNode.hasNonNull("signalingKey")) {
272 signalingKey = rootNode.get("signalingKey").asText();
273 if (signalingKey.equals("null")) {
274 // Workaround for load bug in older versions
275 signalingKey = null;
276 }
277 }
278 if (rootNode.hasNonNull("preKeyIdOffset")) {
279 preKeyIdOffset = rootNode.get("preKeyIdOffset").asInt(0);
280 } else {
281 preKeyIdOffset = 0;
282 }
283 if (rootNode.hasNonNull("nextSignedPreKeyId")) {
284 nextSignedPreKeyId = rootNode.get("nextSignedPreKeyId").asInt();
285 } else {
286 nextSignedPreKeyId = 0;
287 }
288 if (rootNode.hasNonNull("profileKey")) {
289 try {
290 profileKey = new ProfileKey(Base64.getDecoder().decode(rootNode.get("profileKey").asText()));
291 } catch (InvalidInputException e) {
292 throw new IOException(
293 "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
294 e);
295 }
296 }
297
298 signalProtocolStore = jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
299 JsonSignalProtocolStore.class);
300 registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
301 JsonNode groupStoreNode = rootNode.get("groupStore");
302 if (groupStoreNode != null) {
303 groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
304 groupStore.groupCachePath = getGroupCachePath(dataPath, username);
305 }
306 if (groupStore == null) {
307 groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
308 }
309
310 JsonNode contactStoreNode = rootNode.get("contactStore");
311 if (contactStoreNode != null) {
312 contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
313 }
314 if (contactStore == null) {
315 contactStore = new JsonContactsStore();
316 }
317
318 JsonNode recipientStoreNode = rootNode.get("recipientStore");
319 if (recipientStoreNode != null) {
320 recipientStore = jsonProcessor.convertValue(recipientStoreNode, RecipientStore.class);
321 }
322 if (recipientStore == null) {
323 recipientStore = new RecipientStore();
324
325 recipientStore.resolveServiceAddress(getSelfAddress());
326
327 for (ContactInfo contact : contactStore.getContacts()) {
328 recipientStore.resolveServiceAddress(contact.getAddress());
329 }
330
331 for (GroupInfo group : groupStore.getGroups()) {
332 if (group instanceof GroupInfoV1) {
333 GroupInfoV1 groupInfoV1 = (GroupInfoV1) group;
334 groupInfoV1.members = groupInfoV1.members.stream()
335 .map(m -> recipientStore.resolveServiceAddress(m))
336 .collect(Collectors.toSet());
337 }
338 }
339
340 for (SessionInfo session : signalProtocolStore.getSessions()) {
341 session.address = recipientStore.resolveServiceAddress(session.address);
342 }
343
344 for (IdentityInfo identity : signalProtocolStore.getIdentities()) {
345 identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress()));
346 }
347 }
348
349 JsonNode profileStoreNode = rootNode.get("profileStore");
350 if (profileStoreNode != null) {
351 profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class);
352 }
353 if (profileStore == null) {
354 profileStore = new ProfileStore();
355 }
356
357 JsonNode stickerStoreNode = rootNode.get("stickerStore");
358 if (stickerStoreNode != null) {
359 stickerStore = jsonProcessor.convertValue(stickerStoreNode, StickerStore.class);
360 }
361 if (stickerStore == null) {
362 stickerStore = new StickerStore();
363 }
364
365 messageCache = new MessageCache(getMessageCachePath(dataPath, username));
366
367 JsonNode threadStoreNode = rootNode.get("threadStore");
368 if (threadStoreNode != null && !threadStoreNode.isNull()) {
369 LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode,
370 LegacyJsonThreadStore.class);
371 // Migrate thread info to group and contact store
372 for (ThreadInfo thread : threadStore.getThreads()) {
373 if (thread.id == null || thread.id.isEmpty()) {
374 continue;
375 }
376 try {
377 ContactInfo contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
378 if (contactInfo != null) {
379 contactInfo.messageExpirationTime = thread.messageExpirationTime;
380 contactStore.updateContact(contactInfo);
381 } else {
382 GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
383 if (groupInfo instanceof GroupInfoV1) {
384 ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
385 groupStore.updateGroup(groupInfo);
386 }
387 }
388 } catch (Exception ignored) {
389 }
390 }
391 }
392 }
393
394 public void save() {
395 if (fileChannel == null) {
396 return;
397 }
398 ObjectNode rootNode = jsonProcessor.createObjectNode();
399 rootNode.put("username", username)
400 .put("uuid", uuid == null ? null : uuid.toString())
401 .put("deviceId", deviceId)
402 .put("isMultiDevice", isMultiDevice)
403 .put("password", password)
404 .put("registrationLockPin", registrationLockPin)
405 .put("pinMasterKey",
406 pinMasterKey == null ? null : Base64.getEncoder().encodeToString(pinMasterKey.serialize()))
407 .put("storageKey",
408 storageKey == null ? null : Base64.getEncoder().encodeToString(storageKey.serialize()))
409 .put("signalingKey", signalingKey)
410 .put("preKeyIdOffset", preKeyIdOffset)
411 .put("nextSignedPreKeyId", nextSignedPreKeyId)
412 .put("profileKey", Base64.getEncoder().encodeToString(profileKey.serialize()))
413 .put("registered", registered)
414 .putPOJO("axolotlStore", signalProtocolStore)
415 .putPOJO("groupStore", groupStore)
416 .putPOJO("contactStore", contactStore)
417 .putPOJO("recipientStore", recipientStore)
418 .putPOJO("profileStore", profileStore)
419 .putPOJO("stickerStore", stickerStore);
420 try {
421 try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
422 // Write to memory first to prevent corrupting the file in case of serialization errors
423 jsonProcessor.writeValue(output, rootNode);
424 ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
425 synchronized (fileChannel) {
426 fileChannel.position(0);
427 input.transferTo(Channels.newOutputStream(fileChannel));
428 fileChannel.truncate(fileChannel.position());
429 fileChannel.force(false);
430 }
431 }
432 } catch (Exception e) {
433 logger.error("Error saving file: {}", e.getMessage());
434 }
435 }
436
437 private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
438 FileChannel fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
439 FileLock lock = fileChannel.tryLock();
440 if (lock == null) {
441 logger.info("Config file is in use by another instance, waiting…");
442 lock = fileChannel.lock();
443 logger.info("Config file lock acquired.");
444 }
445 return new Pair<>(fileChannel, lock);
446 }
447
448 public void setResolver(final SignalServiceAddressResolver resolver) {
449 signalProtocolStore.setResolver(resolver);
450 }
451
452 public void addPreKeys(Collection<PreKeyRecord> records) {
453 for (PreKeyRecord record : records) {
454 signalProtocolStore.storePreKey(record.getId(), record);
455 }
456 preKeyIdOffset = (preKeyIdOffset + records.size()) % Medium.MAX_VALUE;
457 }
458
459 public void addSignedPreKey(SignedPreKeyRecord record) {
460 signalProtocolStore.storeSignedPreKey(record.getId(), record);
461 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
462 }
463
464 public JsonSignalProtocolStore getSignalProtocolStore() {
465 return signalProtocolStore;
466 }
467
468 public JsonGroupStore getGroupStore() {
469 return groupStore;
470 }
471
472 public JsonContactsStore getContactStore() {
473 return contactStore;
474 }
475
476 public RecipientStore getRecipientStore() {
477 return recipientStore;
478 }
479
480 public ProfileStore getProfileStore() {
481 return profileStore;
482 }
483
484 public StickerStore getStickerStore() {
485 return stickerStore;
486 }
487
488 public MessageCache getMessageCache() {
489 return messageCache;
490 }
491
492 public String getUsername() {
493 return username;
494 }
495
496 public UUID getUuid() {
497 return uuid;
498 }
499
500 public void setUuid(final UUID uuid) {
501 this.uuid = uuid;
502 }
503
504 public SignalServiceAddress getSelfAddress() {
505 return new SignalServiceAddress(uuid, username);
506 }
507
508 public int getDeviceId() {
509 return deviceId;
510 }
511
512 public void setDeviceId(final int deviceId) {
513 this.deviceId = deviceId;
514 }
515
516 public boolean isMasterDevice() {
517 return deviceId == SignalServiceAddress.DEFAULT_DEVICE_ID;
518 }
519
520 public String getPassword() {
521 return password;
522 }
523
524 public void setPassword(final String password) {
525 this.password = password;
526 }
527
528 public String getRegistrationLockPin() {
529 return registrationLockPin;
530 }
531
532 public void setRegistrationLockPin(final String registrationLockPin) {
533 this.registrationLockPin = registrationLockPin;
534 }
535
536 public MasterKey getPinMasterKey() {
537 return pinMasterKey;
538 }
539
540 public void setPinMasterKey(final MasterKey pinMasterKey) {
541 this.pinMasterKey = pinMasterKey;
542 }
543
544 public StorageKey getStorageKey() {
545 if (pinMasterKey != null) {
546 return pinMasterKey.deriveStorageServiceKey();
547 }
548 return storageKey;
549 }
550
551 public void setStorageKey(final StorageKey storageKey) {
552 this.storageKey = storageKey;
553 }
554
555 public String getSignalingKey() {
556 return signalingKey;
557 }
558
559 public void setSignalingKey(final String signalingKey) {
560 this.signalingKey = signalingKey;
561 }
562
563 public ProfileKey getProfileKey() {
564 return profileKey;
565 }
566
567 public void setProfileKey(final ProfileKey profileKey) {
568 this.profileKey = profileKey;
569 }
570
571 public byte[] getSelfUnidentifiedAccessKey() {
572 return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
573 }
574
575 public int getPreKeyIdOffset() {
576 return preKeyIdOffset;
577 }
578
579 public int getNextSignedPreKeyId() {
580 return nextSignedPreKeyId;
581 }
582
583 public boolean isRegistered() {
584 return registered;
585 }
586
587 public void setRegistered(final boolean registered) {
588 this.registered = registered;
589 }
590
591 public boolean isMultiDevice() {
592 return isMultiDevice;
593 }
594
595 public void setMultiDevice(final boolean multiDevice) {
596 isMultiDevice = multiDevice;
597 }
598
599 public boolean isUnrestrictedUnidentifiedAccess() {
600 // TODO make configurable
601 return false;
602 }
603
604 public boolean isDiscoverableByPhoneNumber() {
605 // TODO make configurable
606 return true;
607 }
608
609 @Override
610 public void close() throws IOException {
611 if (fileChannel.isOpen()) {
612 save();
613 }
614 synchronized (fileChannel) {
615 try {
616 lock.close();
617 } catch (ClosedChannelException ignored) {
618 }
619 fileChannel.close();
620 }
621 }
622 }