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