]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/storage/SignalAccount.java
Use SignalServiceAddress in more places
[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 import java.util.UUID;
36
37 public class SignalAccount {
38
39 private final ObjectMapper jsonProcessor = new ObjectMapper();
40 private FileChannel fileChannel;
41 private FileLock lock;
42 private String username;
43 private UUID uuid;
44 private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
45 private boolean isMultiDevice = false;
46 private String password;
47 private String registrationLockPin;
48 private String signalingKey;
49 private ProfileKey profileKey;
50 private int preKeyIdOffset;
51 private int nextSignedPreKeyId;
52
53 private boolean registered = false;
54
55 private JsonSignalProtocolStore signalProtocolStore;
56 private JsonGroupStore groupStore;
57 private JsonContactsStore contactStore;
58 private JsonThreadStore threadStore;
59
60 private SignalAccount() {
61 jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
62 jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
63 jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
64 jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
65 jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
66 jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
67 }
68
69 public static SignalAccount load(String dataPath, String username) throws IOException {
70 SignalAccount account = new SignalAccount();
71 IOUtils.createPrivateDirectories(dataPath);
72 account.openFileChannel(getFileName(dataPath, username));
73 account.load();
74 return account;
75 }
76
77 public static SignalAccount create(String dataPath, String username, IdentityKeyPair identityKey, int registrationId, ProfileKey profileKey) throws IOException {
78 IOUtils.createPrivateDirectories(dataPath);
79
80 SignalAccount account = new SignalAccount();
81 account.openFileChannel(getFileName(dataPath, username));
82
83 account.username = username;
84 account.profileKey = profileKey;
85 account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
86 account.groupStore = new JsonGroupStore();
87 account.threadStore = new JsonThreadStore();
88 account.contactStore = new JsonContactsStore();
89 account.registered = false;
90
91 return account;
92 }
93
94 public static SignalAccount createLinkedAccount(String dataPath, String username, String password, int deviceId, IdentityKeyPair identityKey, int registrationId, String signalingKey, ProfileKey profileKey) throws IOException {
95 IOUtils.createPrivateDirectories(dataPath);
96
97 SignalAccount account = new SignalAccount();
98 account.openFileChannel(getFileName(dataPath, username));
99
100 account.username = username;
101 account.password = password;
102 account.profileKey = profileKey;
103 account.deviceId = deviceId;
104 account.signalingKey = signalingKey;
105 account.signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
106 account.groupStore = new JsonGroupStore();
107 account.threadStore = new JsonThreadStore();
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 threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class);
195 }
196 if (threadStore == null) {
197 threadStore = new JsonThreadStore();
198 }
199 }
200
201 public void save() {
202 if (fileChannel == null) {
203 return;
204 }
205 ObjectNode rootNode = jsonProcessor.createObjectNode();
206 rootNode.put("username", username)
207 .put("deviceId", deviceId)
208 .put("isMultiDevice", isMultiDevice)
209 .put("password", password)
210 .put("registrationLockPin", registrationLockPin)
211 .put("signalingKey", signalingKey)
212 .put("preKeyIdOffset", preKeyIdOffset)
213 .put("nextSignedPreKeyId", nextSignedPreKeyId)
214 .put("profileKey", Base64.encodeBytes(profileKey.serialize()))
215 .put("registered", registered)
216 .putPOJO("axolotlStore", signalProtocolStore)
217 .putPOJO("groupStore", groupStore)
218 .putPOJO("contactStore", contactStore)
219 .putPOJO("threadStore", threadStore)
220 ;
221 try {
222 synchronized (fileChannel) {
223 fileChannel.position(0);
224 jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode);
225 fileChannel.truncate(fileChannel.position());
226 fileChannel.force(false);
227 }
228 } catch (Exception e) {
229 System.err.println(String.format("Error saving file: %s", e.getMessage()));
230 }
231 }
232
233 private void openFileChannel(String fileName) throws IOException {
234 if (fileChannel != null) {
235 return;
236 }
237
238 if (!new File(fileName).exists()) {
239 IOUtils.createPrivateFile(fileName);
240 }
241 fileChannel = new RandomAccessFile(new File(fileName), "rw").getChannel();
242 lock = fileChannel.tryLock();
243 if (lock == null) {
244 System.err.println("Config file is in use by another instance, waiting…");
245 lock = fileChannel.lock();
246 System.err.println("Config file lock acquired.");
247 }
248 }
249
250 public void addPreKeys(Collection<PreKeyRecord> records) {
251 for (PreKeyRecord record : records) {
252 signalProtocolStore.storePreKey(record.getId(), record);
253 }
254 preKeyIdOffset = (preKeyIdOffset + records.size()) % Medium.MAX_VALUE;
255 }
256
257 public void addSignedPreKey(SignedPreKeyRecord record) {
258 signalProtocolStore.storeSignedPreKey(record.getId(), record);
259 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
260 }
261
262 public JsonSignalProtocolStore getSignalProtocolStore() {
263 return signalProtocolStore;
264 }
265
266 public JsonGroupStore getGroupStore() {
267 return groupStore;
268 }
269
270 public JsonContactsStore getContactStore() {
271 return contactStore;
272 }
273
274 public JsonThreadStore getThreadStore() {
275 return threadStore;
276 }
277
278 public String getUsername() {
279 return username;
280 }
281
282 public UUID getUuid() {
283 return uuid;
284 }
285
286 public SignalServiceAddress getSelfAddress() {
287 return new SignalServiceAddress(uuid, username);
288 }
289
290 public int getDeviceId() {
291 return deviceId;
292 }
293
294 public String getPassword() {
295 return password;
296 }
297
298 public void setPassword(final String password) {
299 this.password = password;
300 }
301
302 public String getRegistrationLockPin() {
303 return registrationLockPin;
304 }
305
306 public String getRegistrationLock() {
307 return null; // TODO implement KBS
308 }
309
310 public void setRegistrationLockPin(final String registrationLockPin) {
311 this.registrationLockPin = registrationLockPin;
312 }
313
314 public String getSignalingKey() {
315 return signalingKey;
316 }
317
318 public void setSignalingKey(final String signalingKey) {
319 this.signalingKey = signalingKey;
320 }
321
322 public ProfileKey getProfileKey() {
323 return profileKey;
324 }
325
326 public void setProfileKey(final ProfileKey profileKey) {
327 this.profileKey = profileKey;
328 }
329
330 public int getPreKeyIdOffset() {
331 return preKeyIdOffset;
332 }
333
334 public int getNextSignedPreKeyId() {
335 return nextSignedPreKeyId;
336 }
337
338 public boolean isRegistered() {
339 return registered;
340 }
341
342 public void setRegistered(final boolean registered) {
343 this.registered = registered;
344 }
345
346 public boolean isMultiDevice() {
347 return isMultiDevice;
348 }
349
350 public void setMultiDevice(final boolean multiDevice) {
351 isMultiDevice = multiDevice;
352 }
353 }