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