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