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