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