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