]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/storage/SignalAccount.java
e697efe35d1a7898c960ba0da50d3db2356c22e6
[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.manager.GroupId;
14 import org.asamk.signal.storage.contacts.ContactInfo;
15 import org.asamk.signal.storage.contacts.JsonContactsStore;
16 import org.asamk.signal.storage.groups.GroupInfo;
17 import org.asamk.signal.storage.groups.GroupInfoV1;
18 import org.asamk.signal.storage.groups.JsonGroupStore;
19 import org.asamk.signal.storage.profiles.ProfileStore;
20 import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
21 import org.asamk.signal.storage.protocol.JsonSignalProtocolStore;
22 import org.asamk.signal.storage.protocol.RecipientStore;
23 import org.asamk.signal.storage.protocol.SessionInfo;
24 import org.asamk.signal.storage.protocol.SignalServiceAddressResolver;
25 import org.asamk.signal.storage.stickers.StickerStore;
26 import org.asamk.signal.storage.threads.LegacyJsonThreadStore;
27 import org.asamk.signal.storage.threads.ThreadInfo;
28 import org.asamk.signal.util.IOUtils;
29 import org.asamk.signal.util.Util;
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(String dataPath, String username) throws IOException {
94 final String 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 String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey
109 ) throws IOException {
110 IOUtils.createPrivateDirectories(dataPath);
111 String fileName = getFileName(dataPath, username);
112 if (!new File(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 String 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 String fileName = getFileName(dataPath, username);
145 if (!new File(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 String getFileName(String dataPath, String username) {
171 return dataPath + "/" + username;
172 }
173
174 private static File getGroupCachePath(String dataPath, String username) {
175 return new File(new File(dataPath, username + ".d"), "group-cache");
176 }
177
178 public static boolean userExists(String dataPath, String username) {
179 if (username == null) {
180 return false;
181 }
182 File f = new File(getFileName(dataPath, username));
183 return !(!f.exists() || f.isDirectory());
184 }
185
186 private void load(String dataPath) throws IOException {
187 JsonNode rootNode;
188 synchronized (fileChannel) {
189 fileChannel.position(0);
190 rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
191 }
192
193 JsonNode uuidNode = rootNode.get("uuid");
194 if (uuidNode != null && !uuidNode.isNull()) {
195 try {
196 uuid = UUID.fromString(uuidNode.asText());
197 } catch (IllegalArgumentException e) {
198 throw new IOException("Config file contains an invalid uuid, needs to be a valid UUID", e);
199 }
200 }
201 JsonNode node = rootNode.get("deviceId");
202 if (node != null) {
203 deviceId = node.asInt();
204 }
205 if (rootNode.has("isMultiDevice")) {
206 isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean();
207 }
208 username = Util.getNotNullNode(rootNode, "username").asText();
209 password = Util.getNotNullNode(rootNode, "password").asText();
210 JsonNode pinNode = rootNode.get("registrationLockPin");
211 registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText();
212 if (rootNode.has("signalingKey")) {
213 signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText();
214 }
215 if (rootNode.has("preKeyIdOffset")) {
216 preKeyIdOffset = Util.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
217 } else {
218 preKeyIdOffset = 0;
219 }
220 if (rootNode.has("nextSignedPreKeyId")) {
221 nextSignedPreKeyId = Util.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
222 } else {
223 nextSignedPreKeyId = 0;
224 }
225 if (rootNode.has("profileKey")) {
226 try {
227 profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()));
228 } catch (InvalidInputException e) {
229 throw new IOException(
230 "Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes",
231 e);
232 }
233 }
234
235 signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"),
236 JsonSignalProtocolStore.class);
237 registered = Util.getNotNullNode(rootNode, "registered").asBoolean();
238 JsonNode groupStoreNode = rootNode.get("groupStore");
239 if (groupStoreNode != null) {
240 groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
241 groupStore.groupCachePath = getGroupCachePath(dataPath, username);
242 }
243 if (groupStore == null) {
244 groupStore = new JsonGroupStore(getGroupCachePath(dataPath, username));
245 }
246
247 JsonNode contactStoreNode = rootNode.get("contactStore");
248 if (contactStoreNode != null) {
249 contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
250 }
251 if (contactStore == null) {
252 contactStore = new JsonContactsStore();
253 }
254
255 JsonNode recipientStoreNode = rootNode.get("recipientStore");
256 if (recipientStoreNode != null) {
257 recipientStore = jsonProcessor.convertValue(recipientStoreNode, RecipientStore.class);
258 }
259 if (recipientStore == null) {
260 recipientStore = new RecipientStore();
261
262 recipientStore.resolveServiceAddress(getSelfAddress());
263
264 for (ContactInfo contact : contactStore.getContacts()) {
265 recipientStore.resolveServiceAddress(contact.getAddress());
266 }
267
268 for (GroupInfo group : groupStore.getGroups()) {
269 if (group instanceof GroupInfoV1) {
270 GroupInfoV1 groupInfoV1 = (GroupInfoV1) group;
271 groupInfoV1.members = groupInfoV1.members.stream()
272 .map(m -> recipientStore.resolveServiceAddress(m))
273 .collect(Collectors.toSet());
274 }
275 }
276
277 for (SessionInfo session : signalProtocolStore.getSessions()) {
278 session.address = recipientStore.resolveServiceAddress(session.address);
279 }
280
281 for (JsonIdentityKeyStore.Identity identity : signalProtocolStore.getIdentities()) {
282 identity.setAddress(recipientStore.resolveServiceAddress(identity.getAddress()));
283 }
284 }
285
286 JsonNode profileStoreNode = rootNode.get("profileStore");
287 if (profileStoreNode != null) {
288 profileStore = jsonProcessor.convertValue(profileStoreNode, ProfileStore.class);
289 }
290 if (profileStore == null) {
291 profileStore = new ProfileStore();
292 }
293
294 JsonNode stickerStoreNode = rootNode.get("stickerStore");
295 if (stickerStoreNode != null) {
296 stickerStore = jsonProcessor.convertValue(stickerStoreNode, StickerStore.class);
297 }
298 if (stickerStore == null) {
299 stickerStore = new StickerStore();
300 }
301
302 JsonNode threadStoreNode = rootNode.get("threadStore");
303 if (threadStoreNode != null) {
304 LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode,
305 LegacyJsonThreadStore.class);
306 // Migrate thread info to group and contact store
307 for (ThreadInfo thread : threadStore.getThreads()) {
308 if (thread.id == null || thread.id.isEmpty()) {
309 continue;
310 }
311 try {
312 ContactInfo contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
313 if (contactInfo != null) {
314 contactInfo.messageExpirationTime = thread.messageExpirationTime;
315 contactStore.updateContact(contactInfo);
316 } else {
317 GroupInfo groupInfo = groupStore.getGroup(GroupId.fromBase64(thread.id));
318 if (groupInfo instanceof GroupInfoV1) {
319 ((GroupInfoV1) groupInfo).messageExpirationTime = thread.messageExpirationTime;
320 groupStore.updateGroup(groupInfo);
321 }
322 }
323 } catch (Exception ignored) {
324 }
325 }
326 }
327 }
328
329 public void save() {
330 if (fileChannel == null) {
331 return;
332 }
333 ObjectNode rootNode = jsonProcessor.createObjectNode();
334 rootNode.put("username", username)
335 .put("uuid", uuid == null ? null : uuid.toString())
336 .put("deviceId", deviceId)
337 .put("isMultiDevice", isMultiDevice)
338 .put("password", password)
339 .put("registrationLockPin", registrationLockPin)
340 .put("signalingKey", signalingKey)
341 .put("preKeyIdOffset", preKeyIdOffset)
342 .put("nextSignedPreKeyId", nextSignedPreKeyId)
343 .put("profileKey", Base64.encodeBytes(profileKey.serialize()))
344 .put("registered", registered)
345 .putPOJO("axolotlStore", signalProtocolStore)
346 .putPOJO("groupStore", groupStore)
347 .putPOJO("contactStore", contactStore)
348 .putPOJO("recipientStore", recipientStore)
349 .putPOJO("profileStore", profileStore)
350 .putPOJO("stickerStore", stickerStore);
351 try {
352 try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
353 // Write to memory first to prevent corrupting the file in case of serialization errors
354 jsonProcessor.writeValue(output, rootNode);
355 ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray());
356 synchronized (fileChannel) {
357 fileChannel.position(0);
358 input.transferTo(Channels.newOutputStream(fileChannel));
359 fileChannel.truncate(fileChannel.position());
360 fileChannel.force(false);
361 }
362 }
363 } catch (Exception e) {
364 logger.error("Error saving file: {}", e.getMessage());
365 }
366 }
367
368 private static Pair<FileChannel, FileLock> openFileChannel(String fileName) throws IOException {
369 FileChannel fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
370 FileLock lock = fileChannel.tryLock();
371 if (lock == null) {
372 logger.info("Config file is in use by another instance, waiting…");
373 lock = fileChannel.lock();
374 logger.info("Config file lock acquired.");
375 }
376 return new Pair<>(fileChannel, lock);
377 }
378
379 public void setResolver(final SignalServiceAddressResolver resolver) {
380 signalProtocolStore.setResolver(resolver);
381 }
382
383 public void addPreKeys(Collection<PreKeyRecord> records) {
384 for (PreKeyRecord record : records) {
385 signalProtocolStore.storePreKey(record.getId(), record);
386 }
387 preKeyIdOffset = (preKeyIdOffset + records.size()) % Medium.MAX_VALUE;
388 }
389
390 public void addSignedPreKey(SignedPreKeyRecord record) {
391 signalProtocolStore.storeSignedPreKey(record.getId(), record);
392 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
393 }
394
395 public JsonSignalProtocolStore getSignalProtocolStore() {
396 return signalProtocolStore;
397 }
398
399 public JsonGroupStore getGroupStore() {
400 return groupStore;
401 }
402
403 public JsonContactsStore getContactStore() {
404 return contactStore;
405 }
406
407 public RecipientStore getRecipientStore() {
408 return recipientStore;
409 }
410
411 public ProfileStore getProfileStore() {
412 return profileStore;
413 }
414
415 public StickerStore getStickerStore() {
416 return stickerStore;
417 }
418
419 public String getUsername() {
420 return username;
421 }
422
423 public UUID getUuid() {
424 return uuid;
425 }
426
427 public void setUuid(final UUID uuid) {
428 this.uuid = uuid;
429 }
430
431 public SignalServiceAddress getSelfAddress() {
432 return new SignalServiceAddress(uuid, username);
433 }
434
435 public int getDeviceId() {
436 return deviceId;
437 }
438
439 public String getPassword() {
440 return password;
441 }
442
443 public void setPassword(final String password) {
444 this.password = password;
445 }
446
447 public String getRegistrationLockPin() {
448 return registrationLockPin;
449 }
450
451 public String getRegistrationLock() {
452 return null; // TODO implement KBS
453 }
454
455 public void setRegistrationLockPin(final String registrationLockPin) {
456 this.registrationLockPin = registrationLockPin;
457 }
458
459 public String getSignalingKey() {
460 return signalingKey;
461 }
462
463 public void setSignalingKey(final String signalingKey) {
464 this.signalingKey = signalingKey;
465 }
466
467 public ProfileKey getProfileKey() {
468 return profileKey;
469 }
470
471 public void setProfileKey(final ProfileKey profileKey) {
472 this.profileKey = profileKey;
473 }
474
475 public int getPreKeyIdOffset() {
476 return preKeyIdOffset;
477 }
478
479 public int getNextSignedPreKeyId() {
480 return nextSignedPreKeyId;
481 }
482
483 public boolean isRegistered() {
484 return registered;
485 }
486
487 public void setRegistered(final boolean registered) {
488 this.registered = registered;
489 }
490
491 public boolean isMultiDevice() {
492 return isMultiDevice;
493 }
494
495 public void setMultiDevice(final boolean multiDevice) {
496 isMultiDevice = multiDevice;
497 }
498
499 @Override
500 public void close() throws IOException {
501 synchronized (fileChannel) {
502 try {
503 lock.close();
504 } catch (ClosedChannelException ignored) {
505 }
506 fileChannel.close();
507 }
508 }
509 }