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