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