]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/storage/SignalAccount.java
6d83f78f999933ac19d498d802adb64e0ea74bd5
[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, 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.password = password;
103 account.profileKey = profileKey;
104 account.deviceId = deviceId;
105 account.signalingKey = signalingKey;
106 account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
107 account.groupStore = new JsonGroupStore();
108 account.contactStore = new JsonContactsStore();
109 account.registered = true;
110 account.isMultiDevice = true;
111
112 return account;
113 }
114
115 public static SignalAccount createTemporaryAccount(IdentityKeyPair identityKey, int registrationId) {
116 SignalAccount account = new SignalAccount();
117
118 account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
119 account.registered = false;
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 node = rootNode.get("deviceId");
144 if (node != null) {
145 deviceId = node.asInt();
146 }
147 if (rootNode.has("isMultiDevice")) {
148 isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean();
149 }
150 username = Util.getNotNullNode(rootNode, "username").asText();
151 password = Util.getNotNullNode(rootNode, "password").asText();
152 JsonNode pinNode = rootNode.get("registrationLockPin");
153 registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText();
154 if (rootNode.has("signalingKey")) {
155 signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText();
156 }
157 if (rootNode.has("preKeyIdOffset")) {
158 preKeyIdOffset = Util.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
159 } else {
160 preKeyIdOffset = 0;
161 }
162 if (rootNode.has("nextSignedPreKeyId")) {
163 nextSignedPreKeyId = Util.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
164 } else {
165 nextSignedPreKeyId = 0;
166 }
167 if (rootNode.has("profileKey")) {
168 try {
169 profileKey = new ProfileKey(Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText()));
170 } catch (InvalidInputException e) {
171 throw new IOException("Config file contains an invalid profileKey, needs to be base64 encoded array of 32 bytes", e);
172 }
173 }
174
175 signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
176 registered = Util.getNotNullNode(rootNode, "registered").asBoolean();
177 JsonNode groupStoreNode = rootNode.get("groupStore");
178 if (groupStoreNode != null) {
179 groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
180 }
181 if (groupStore == null) {
182 groupStore = new JsonGroupStore();
183 }
184
185 JsonNode contactStoreNode = rootNode.get("contactStore");
186 if (contactStoreNode != null) {
187 contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
188 }
189 if (contactStore == null) {
190 contactStore = new JsonContactsStore();
191 }
192 JsonNode threadStoreNode = rootNode.get("threadStore");
193 if (threadStoreNode != null) {
194 LegacyJsonThreadStore threadStore = jsonProcessor.convertValue(threadStoreNode, LegacyJsonThreadStore.class);
195 // Migrate thread info to group and contact store
196 for (ThreadInfo thread : threadStore.getThreads()) {
197 try {
198 ContactInfo contactInfo = contactStore.getContact(new SignalServiceAddress(null, thread.id));
199 if (contactInfo != null) {
200 contactInfo.messageExpirationTime = thread.messageExpirationTime;
201 contactStore.updateContact(contactInfo);
202 } else {
203 GroupInfo groupInfo = groupStore.getGroup(Base64.decode(thread.id));
204 if (groupInfo != null) {
205 groupInfo.messageExpirationTime = thread.messageExpirationTime;
206 groupStore.updateGroup(groupInfo);
207 }
208 }
209 } catch (Exception ignored) {
210 }
211 }
212 }
213 }
214
215 public void save() {
216 if (fileChannel == null) {
217 return;
218 }
219 ObjectNode rootNode = jsonProcessor.createObjectNode();
220 rootNode.put("username", username)
221 .put("deviceId", deviceId)
222 .put("isMultiDevice", isMultiDevice)
223 .put("password", password)
224 .put("registrationLockPin", registrationLockPin)
225 .put("signalingKey", signalingKey)
226 .put("preKeyIdOffset", preKeyIdOffset)
227 .put("nextSignedPreKeyId", nextSignedPreKeyId)
228 .put("profileKey", Base64.encodeBytes(profileKey.serialize()))
229 .put("registered", registered)
230 .putPOJO("axolotlStore", signalProtocolStore)
231 .putPOJO("groupStore", groupStore)
232 .putPOJO("contactStore", contactStore)
233 ;
234 try {
235 synchronized (fileChannel) {
236 fileChannel.position(0);
237 jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode);
238 fileChannel.truncate(fileChannel.position());
239 fileChannel.force(false);
240 }
241 } catch (Exception e) {
242 System.err.println(String.format("Error saving file: %s", e.getMessage()));
243 }
244 }
245
246 private void openFileChannel(String fileName) throws IOException {
247 if (fileChannel != null) {
248 return;
249 }
250
251 if (!new File(fileName).exists()) {
252 IOUtils.createPrivateFile(fileName);
253 }
254 fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
255 lock = fileChannel.tryLock();
256 if (lock == null) {
257 System.err.println("Config file is in use by another instance, waiting…");
258 lock = fileChannel.lock();
259 System.err.println("Config file lock acquired.");
260 }
261 }
262
263 public void addPreKeys(Collection<PreKeyRecord> records) {
264 for (PreKeyRecord record : records) {
265 signalProtocolStore.storePreKey(record.getId(), record);
266 }
267 preKeyIdOffset = (preKeyIdOffset + records.size()) % Medium.MAX_VALUE;
268 }
269
270 public void addSignedPreKey(SignedPreKeyRecord record) {
271 signalProtocolStore.storeSignedPreKey(record.getId(), record);
272 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
273 }
274
275 public JsonSignalProtocolStore getSignalProtocolStore() {
276 return signalProtocolStore;
277 }
278
279 public JsonGroupStore getGroupStore() {
280 return groupStore;
281 }
282
283 public JsonContactsStore getContactStore() {
284 return contactStore;
285 }
286
287 public String getUsername() {
288 return username;
289 }
290
291 public UUID getUuid() {
292 return uuid;
293 }
294
295 public SignalServiceAddress getSelfAddress() {
296 return new SignalServiceAddress(uuid, username);
297 }
298
299 public int getDeviceId() {
300 return deviceId;
301 }
302
303 public String getPassword() {
304 return password;
305 }
306
307 public void setPassword(final String password) {
308 this.password = password;
309 }
310
311 public String getRegistrationLockPin() {
312 return registrationLockPin;
313 }
314
315 public String getRegistrationLock() {
316 return null; // TODO implement KBS
317 }
318
319 public void setRegistrationLockPin(final String registrationLockPin) {
320 this.registrationLockPin = registrationLockPin;
321 }
322
323 public String getSignalingKey() {
324 return signalingKey;
325 }
326
327 public void setSignalingKey(final String signalingKey) {
328 this.signalingKey = signalingKey;
329 }
330
331 public ProfileKey getProfileKey() {
332 return profileKey;
333 }
334
335 public void setProfileKey(final ProfileKey profileKey) {
336 this.profileKey = profileKey;
337 }
338
339 public int getPreKeyIdOffset() {
340 return preKeyIdOffset;
341 }
342
343 public int getNextSignedPreKeyId() {
344 return nextSignedPreKeyId;
345 }
346
347 public boolean isRegistered() {
348 return registered;
349 }
350
351 public void setRegistered(final boolean registered) {
352 this.registered = registered;
353 }
354
355 public boolean isMultiDevice() {
356 return isMultiDevice;
357 }
358
359 public void setMultiDevice(final boolean multiDevice) {
360 isMultiDevice = multiDevice;
361 }
362 }