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