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