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