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