]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
Remove unnecessary isRegistered checks
[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 import org.whispersystems.util.Base64;
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.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, you can disable it.
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.decode(profileKeyString));
204 } catch (InvalidInputException | IOException e) {
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.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.decode(Utils.getNotNullNode(rootNode, "profileKey").asText()));
284 } catch (InvalidInputException e) {
285 throw new IOException(
286 "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
287 e);
288 }
289 }
290
291 signalProtocolStore = jsonProcessor.convertValue(Utils.getNotNullNode(rootNode, "axolotlStore"),
292 JsonSignalProtocolStore.class);
293 registered = Utils.getNotNullNode(rootNode, "registered").asBoolean();
294 JsonNode groupStoreNode = rootNode.get("groupStore");
295 if (groupStoreNode != null) {
296 groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
297 groupStore.groupCachePath = getGroupCachePath(dataPath, username);
298 }
299 if (groupStore == null) {
300 groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
301 }
302
303 JsonNode contactStoreNode = rootNode.get("contactStore");
304 if (contactStoreNode != null) {
305 contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
306 }
307 if (contactStore == null) {
308 contactStore = new JsonContactsStore();
309 }
310
311 JsonNode recipientStoreNode = rootNode.get("recipientStore");
312 if (recipientStoreNode != null) {
313 recipientStore = jsonProcessor.convertValue(recipientStoreNode, RecipientStore.class);
314 }
315 if (recipientStore == null) {
316 recipientStore = new RecipientStore();
317
318 recipientStore.resolveServiceAddress(getSelfAddress());
319
320 for (ContactInfo contact : contactStore.getContacts()) {
321 recipientStore.resolveServiceAddress(contact.getAddress());
322 }
323
324 for (GroupInfo group : groupStore.getGroups()) {
325 if (group instanceof GroupInfoV1) {
326 GroupInfoV1 groupInfoV1 = (GroupInfoV1) group;
327 groupInfoV1.members = groupInfoV1.members.stream()
328 .map(m -> recipientStore.resolveServiceAddress(m))
329 .collect(Collectors.toSet());
330 }
331 }
332
333 for (SessionInfo session : signalProtocolStore.getSessions()) {
334 session.address = recipientStore.resolveServiceAddress(session.address);
335 }
336
337 for (IdentityInfo identity : signalProtocolStore.getIdentities()) {
338 identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress()));
339 }
340 }
341
342 JsonNode profileStoreNode = rootNode.get("profileStore");
343 if (profileStoreNode != null) {
344 profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class);
345 }
346 if (profileStore == null) {
347 profileStore = new ProfileStore();
348 }
349
350 JsonNode stickerStoreNode = rootNode.get("stickerStore");
351 if (stickerStoreNode != null) {
352 stickerStore = jsonProcessor.convertValue(stickerStoreNode, StickerStore.class);
353 }
354 if (stickerStore == null) {
355 stickerStore = new StickerStore();
356 }
357
358 messageCache = new MessageCache(getMessageCachePath(dataPath, username));
359
360 JsonNode threadStoreNode = rootNode.get("threadStore");
361 if (threadStoreNode != null) {
362 LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode,
363 LegacyJsonThreadStore.class);
364 // Migrate thread info to group and contact store
365 for (ThreadInfo thread : threadStore.getThreads()) {
366 if (thread.id == null || thread.id.isEmpty()) {
367 continue;
368 }
369 try {
370 ContactInfo contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
371 if (contactInfo != null) {
372 contactInfo.messageExpirationTime = thread.messageExpirationTime;
373 contactStore.updateContact(contactInfo);
374 } else {
375 GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
376 if (groupInfo instanceof GroupInfoV1) {
377 ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
378 groupStore.updateGroup(groupInfo);
379 }
380 }
381 } catch (Exception ignored) {
382 }
383 }
384 }
385 }
386
387 public void save() {
388 if (fileChannel == null) {
389 return;
390 }
391 ObjectNode rootNode = jsonProcessor.createObjectNode();
392 rootNode.put("username", username)
393 .put("uuid", uuid == null ? null : uuid.toString())
394 .put("deviceId", deviceId)
395 .put("isMultiDevice", isMultiDevice)
396 .put("password", password)
397 .put("registrationLockPin", registrationLockPin)
398 .put("pinMasterKey", pinMasterKey == null ? null : Base64.encodeBytes(pinMasterKey.serialize()))
399 .put("signalingKey", signalingKey)
400 .put("preKeyIdOffset", preKeyIdOffset)
401 .put("nextSignedPreKeyId", nextSignedPreKeyId)
402 .put("profileKey", Base64.encodeBytes(profileKey.serialize()))
403 .put("registered", registered)
404 .putPOJO("axolotlStore", signalProtocolStore)
405 .putPOJO("groupStore", groupStore)
406 .putPOJO("contactStore", contactStore)
407 .putPOJO("recipientStore", recipientStore)
408 .putPOJO("profileStore", profileStore)
409 .putPOJO("stickerStore", stickerStore);
410 try {
411 try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
412 // Write to memory first to prevent corrupting the file in case of serialization errors
413 jsonProcessor.writeValue(output, rootNode);
414 ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
415 synchronized (fileChannel) {
416 fileChannel.position(0);
417 input.transferTo(Channels.newOutputStream(fileChannel));
418 fileChannel.truncate(fileChannel.position());
419 fileChannel.force(false);
420 }
421 }
422 } catch (Exception e) {
423 logger.error("Error saving file: {}", e.getMessage());
424 }
425 }
426
427 private static Pair<FileChannel, FileLock> openFileChannel(File fileName) throws IOException {
428 FileChannel fileChannel = new RandomAccessFile(fileName, "rw").getChannel();
429 FileLock lock = fileChannel.tryLock();
430 if (lock == null) {
431 logger.info("Config file is in use by another instance, waiting…");
432 lock = fileChannel.lock();
433 logger.info("Config file lock acquired.");
434 }
435 return new Pair<>(fileChannel, lock);
436 }
437
438 public void setResolver(final SignalServiceAddressResolver resolver) {
439 signalProtocolStore.setResolver(resolver);
440 }
441
442 public void addPreKeys(Collection<PreKeyRecord> records) {
443 for (PreKeyRecord record : records) {
444 signalProtocolStore.storePreKey(record.getId(), record);
445 }
446 preKeyIdOffset = (preKeyIdOffset + records.size()) % Medium.MAX_VALUE;
447 }
448
449 public void addSignedPreKey(SignedPreKeyRecord record) {
450 signalProtocolStore.storeSignedPreKey(record.getId(), record);
451 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
452 }
453
454 public JsonSignalProtocolStore getSignalProtocolStore() {
455 return signalProtocolStore;
456 }
457
458 public JsonGroupStore getGroupStore() {
459 return groupStore;
460 }
461
462 public JsonContactsStore getContactStore() {
463 return contactStore;
464 }
465
466 public RecipientStore getRecipientStore() {
467 return recipientStore;
468 }
469
470 public ProfileStore getProfileStore() {
471 return profileStore;
472 }
473
474 public StickerStore getStickerStore() {
475 return stickerStore;
476 }
477
478 public MessageCache getMessageCache() {
479 return messageCache;
480 }
481
482 public String getUsername() {
483 return username;
484 }
485
486 public UUID getUuid() {
487 return uuid;
488 }
489
490 public void setUuid(final UUID uuid) {
491 this.uuid = uuid;
492 }
493
494 public SignalServiceAddress getSelfAddress() {
495 return new SignalServiceAddress(uuid, username);
496 }
497
498 public int getDeviceId() {
499 return deviceId;
500 }
501
502 public void setDeviceId(final int deviceId) {
503 this.deviceId = deviceId;
504 }
505
506 public String getPassword() {
507 return password;
508 }
509
510 public void setPassword(final String password) {
511 this.password = password;
512 }
513
514 public String getRegistrationLockPin() {
515 return registrationLockPin;
516 }
517
518 public void setRegistrationLockPin(final String registrationLockPin) {
519 this.registrationLockPin = registrationLockPin;
520 }
521
522 public MasterKey getPinMasterKey() {
523 return pinMasterKey;
524 }
525
526 public void setPinMasterKey(final MasterKey pinMasterKey) {
527 this.pinMasterKey = pinMasterKey;
528 }
529
530 public String getSignalingKey() {
531 return signalingKey;
532 }
533
534 public void setSignalingKey(final String signalingKey) {
535 this.signalingKey = signalingKey;
536 }
537
538 public ProfileKey getProfileKey() {
539 return profileKey;
540 }
541
542 public void setProfileKey(final ProfileKey profileKey) {
543 this.profileKey = profileKey;
544 }
545
546 public byte[] getSelfUnidentifiedAccessKey() {
547 return UnidentifiedAccess.deriveAccessKeyFrom(getProfileKey());
548 }
549
550 public int getPreKeyIdOffset() {
551 return preKeyIdOffset;
552 }
553
554 public int getNextSignedPreKeyId() {
555 return nextSignedPreKeyId;
556 }
557
558 public boolean isRegistered() {
559 return registered;
560 }
561
562 public void setRegistered(final boolean registered) {
563 this.registered = registered;
564 }
565
566 public boolean isMultiDevice() {
567 return isMultiDevice;
568 }
569
570 public void setMultiDevice(final boolean multiDevice) {
571 isMultiDevice = multiDevice;
572 }
573
574 public boolean isUnrestrictedUnidentifiedAccess() {
575 // TODO make configurable
576 return false;
577 }
578
579 public boolean isDiscoverableByPhoneNumber() {
580 // TODO make configurable
581 return true;
582 }
583
584 @Override
585 public void close() throws IOException {
586 save();
587 synchronized (fileChannel) {
588 try {
589 lock.close();
590 } catch (ClosedChannelException ignored) {
591 }
592 fileChannel.close();
593 }
594 }
595 }