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