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