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