]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/storage/SignalAccount.java
Manager : removeLinkedDevices updates isMultiDevice and saves the account
[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 = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
133
134 JsonNode node = rootNode.get("deviceId");
135 if (node != null) {
136 deviceId = node.asInt();
137 }
138 if (rootNode.has("isMultiDevice")) isMultiDevice = Util.getNotNullNode(rootNode, "isMultiDevice").asBoolean();
139 username = Util.getNotNullNode(rootNode, "username").asText();
140 password = Util.getNotNullNode(rootNode, "password").asText();
141 JsonNode pinNode = rootNode.get("registrationLockPin");
142 registrationLockPin = pinNode == null || pinNode.isNull() ? null : pinNode.asText();
143 if (rootNode.has("signalingKey")) {
144 signalingKey = Util.getNotNullNode(rootNode, "signalingKey").asText();
145 }
146 if (rootNode.has("preKeyIdOffset")) {
147 preKeyIdOffset = Util.getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
148 } else {
149 preKeyIdOffset = 0;
150 }
151 if (rootNode.has("nextSignedPreKeyId")) {
152 nextSignedPreKeyId = Util.getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
153 } else {
154 nextSignedPreKeyId = 0;
155 }
156 if (rootNode.has("profileKey")) {
157 profileKey = Base64.decode(Util.getNotNullNode(rootNode, "profileKey").asText());
158 }
159
160 signalProtocolStore = jsonProcessor.convertValue(Util.getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
161 registered = Util.getNotNullNode(rootNode, "registered").asBoolean();
162 JsonNode groupStoreNode = rootNode.get("groupStore");
163 if (groupStoreNode != null) {
164 groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
165 }
166 if (groupStore == null) {
167 groupStore = new JsonGroupStore();
168 }
169
170 JsonNode contactStoreNode = rootNode.get("contactStore");
171 if (contactStoreNode != null) {
172 contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
173 }
174 if (contactStore == null) {
175 contactStore = new JsonContactsStore();
176 }
177 JsonNode threadStoreNode = rootNode.get("threadStore");
178 if (threadStoreNode != null) {
179 threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class);
180 }
181 if (threadStore == null) {
182 threadStore = new JsonThreadStore();
183 }
184 }
185
186 public void save() {
187 if (fileChannel == null) {
188 return;
189 }
190 ObjectNode rootNode = jsonProcessor.createObjectNode();
191 rootNode.put("username", username)
192 .put("deviceId", deviceId)
193 .put("isMultiDevice", isMultiDevice)
194 .put("password", password)
195 .put("registrationLockPin", registrationLockPin)
196 .put("signalingKey", signalingKey)
197 .put("preKeyIdOffset", preKeyIdOffset)
198 .put("nextSignedPreKeyId", nextSignedPreKeyId)
199 .put("profileKey", Base64.encodeBytes(profileKey))
200 .put("registered", registered)
201 .putPOJO("axolotlStore", signalProtocolStore)
202 .putPOJO("groupStore", groupStore)
203 .putPOJO("contactStore", contactStore)
204 .putPOJO("threadStore", threadStore)
205 ;
206 try {
207 fileChannel.position(0);
208 jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode);
209 fileChannel.truncate(fileChannel.position());
210 fileChannel.force(false);
211 } catch (Exception e) {
212 System.err.println(String.format("Error saving file: %s", e.getMessage()));
213 }
214 }
215
216 private void openFileChannel(String fileName) throws IOException {
217 if (fileChannel != null) {
218 return;
219 }
220
221 if (!new File(fileName).exists()) {
222 IOUtils.createPrivateFile(fileName);
223 }
224 fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
225 lock = fileChannel.tryLock();
226 if (lock == null) {
227 System.err.println("Config file is in use by another instance, waiting…");
228 lock = fileChannel.lock();
229 System.err.println("Config file lock acquired.");
230 }
231 }
232
233 public void addPreKeys(Collection<PreKeyRecord> records) {
234 for (PreKeyRecord record : records) {
235 signalProtocolStore.storePreKey(record.getId(), record);
236 }
237 preKeyIdOffset = (preKeyIdOffset + records.size()) % Medium.MAX_VALUE;
238 }
239
240 public void addSignedPreKey(SignedPreKeyRecord record) {
241 signalProtocolStore.storeSignedPreKey(record.getId(), record);
242 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
243 }
244
245 public JsonSignalProtocolStore getSignalProtocolStore() {
246 return signalProtocolStore;
247 }
248
249 public JsonGroupStore getGroupStore() {
250 return groupStore;
251 }
252
253 public JsonContactsStore getContactStore() {
254 return contactStore;
255 }
256
257 public JsonThreadStore getThreadStore() {
258 return threadStore;
259 }
260
261 public String getUsername() {
262 return username;
263 }
264
265 public int getDeviceId() {
266 return deviceId;
267 }
268
269 public String getPassword() {
270 return password;
271 }
272
273 public void setPassword(final String password) {
274 this.password = password;
275 }
276
277 public String getRegistrationLockPin() {
278 return registrationLockPin;
279 }
280
281 public void setRegistrationLockPin(final String registrationLockPin) {
282 this.registrationLockPin = registrationLockPin;
283 }
284
285 public String getSignalingKey() {
286 return signalingKey;
287 }
288
289 public void setSignalingKey(final String signalingKey) {
290 this.signalingKey = signalingKey;
291 }
292
293 public byte[] getProfileKey() {
294 return profileKey;
295 }
296
297 public void setProfileKey(final byte[] profileKey) {
298 this.profileKey = profileKey;
299 }
300
301 public int getPreKeyIdOffset() {
302 return preKeyIdOffset;
303 }
304
305 public int getNextSignedPreKeyId() {
306 return nextSignedPreKeyId;
307 }
308
309 public boolean isRegistered() {
310 return registered;
311 }
312
313 public void setRegistered(final boolean registered) {
314 this.registered = registered;
315 }
316
317 public boolean isMultiDevice() {
318 return isMultiDevice;
319 }
320
321 public void setMultiDevice(final boolean multiDevice) {
322 isMultiDevice = multiDevice;
323 }
324 }