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