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