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