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