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