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