]> nmode's Git Repositories - signal-cli/blob - src/main/java/cli/Manager.java
Fix groups for upgraded clients
[signal-cli] / src / main / java / cli / Manager.java
1 /**
2 * Copyright (C) 2015 AsamK
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17 package cli;
18
19 import com.fasterxml.jackson.annotation.JsonAutoDetect;
20 import com.fasterxml.jackson.annotation.PropertyAccessor;
21 import com.fasterxml.jackson.databind.DeserializationFeature;
22 import com.fasterxml.jackson.databind.JsonNode;
23 import com.fasterxml.jackson.databind.ObjectMapper;
24 import com.fasterxml.jackson.databind.SerializationFeature;
25 import com.fasterxml.jackson.databind.node.ObjectNode;
26 import org.whispersystems.libaxolotl.*;
27 import org.whispersystems.libaxolotl.ecc.Curve;
28 import org.whispersystems.libaxolotl.ecc.ECKeyPair;
29 import org.whispersystems.libaxolotl.state.PreKeyRecord;
30 import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
31 import org.whispersystems.libaxolotl.util.KeyHelper;
32 import org.whispersystems.libaxolotl.util.Medium;
33 import org.whispersystems.libaxolotl.util.guava.Optional;
34 import org.whispersystems.textsecure.api.TextSecureAccountManager;
35 import org.whispersystems.textsecure.api.TextSecureMessagePipe;
36 import org.whispersystems.textsecure.api.TextSecureMessageReceiver;
37 import org.whispersystems.textsecure.api.TextSecureMessageSender;
38 import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
39 import org.whispersystems.textsecure.api.messages.*;
40 import org.whispersystems.textsecure.api.push.TextSecureAddress;
41 import org.whispersystems.textsecure.api.push.TrustStore;
42 import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
43 import org.whispersystems.textsecure.api.util.InvalidNumberException;
44 import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
45
46 import java.io.*;
47 import java.util.*;
48 import java.util.concurrent.TimeUnit;
49 import java.util.concurrent.TimeoutException;
50
51 class Manager {
52 private final static String URL = "https://textsecure-service.whispersystems.org";
53 private final static TrustStore TRUST_STORE = new WhisperTrustStore();
54
55 public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
56 public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
57 private final static String USER_AGENT = PROJECT_NAME + " " + PROJECT_VERSION;
58
59 private final static String settingsPath = System.getProperty("user.home") + "/.config/textsecure";
60 private final static String dataPath = settingsPath + "/data";
61 private final static String attachmentsPath = settingsPath + "/attachments";
62
63 private final ObjectMapper jsonProcessot = new ObjectMapper();
64 private String username;
65 private String password;
66 private String signalingKey;
67 private int preKeyIdOffset;
68 private int nextSignedPreKeyId;
69
70 private boolean registered = false;
71
72 private JsonAxolotlStore axolotlStore;
73 private TextSecureAccountManager accountManager;
74 private JsonGroupStore groupStore;
75
76 public Manager(String username) {
77 this.username = username;
78 jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
79 jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
80 jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
81 jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
82 }
83
84 public String getFileName() {
85 new File(dataPath).mkdirs();
86 return dataPath + "/" + username;
87 }
88
89 public boolean userExists() {
90 File f = new File(getFileName());
91 return !(!f.exists() || f.isDirectory());
92 }
93
94 public boolean userHasKeys() {
95 return axolotlStore != null;
96 }
97
98 private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
99 JsonNode node = parent.get(name);
100 if (node == null) {
101 throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
102 }
103
104 return node;
105 }
106
107 public void load() throws IOException, InvalidKeyException {
108 JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
109
110 username = getNotNullNode(rootNode, "username").asText();
111 password = getNotNullNode(rootNode, "password").asText();
112 if (rootNode.has("signalingKey")) {
113 signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
114 }
115 if (rootNode.has("preKeyIdOffset")) {
116 preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
117 } else {
118 preKeyIdOffset = 0;
119 }
120 if (rootNode.has("nextSignedPreKeyId")) {
121 nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
122 } else {
123 nextSignedPreKeyId = 0;
124 }
125 axolotlStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonAxolotlStore.class); //new JsonAxolotlStore(in.getJSONObject("axolotlStore"));
126 registered = getNotNullNode(rootNode, "registered").asBoolean();
127 JsonNode groupStoreNode = rootNode.get("groupStore");
128 if (groupStoreNode != null) {
129 groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class);
130 }
131 if (groupStore == null) {
132 groupStore = new JsonGroupStore();
133 }
134 accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
135 }
136
137 public void save() {
138 ObjectNode rootNode = jsonProcessot.createObjectNode();
139 rootNode.put("username", username)
140 .put("password", password)
141 .put("signalingKey", signalingKey)
142 .put("preKeyIdOffset", preKeyIdOffset)
143 .put("nextSignedPreKeyId", nextSignedPreKeyId)
144 .put("registered", registered)
145 .putPOJO("axolotlStore", axolotlStore)
146 .putPOJO("groupStore", groupStore)
147 ;
148 try {
149 jsonProcessot.writeValue(new File(getFileName()), rootNode);
150 } catch (Exception e) {
151 System.err.println(String.format("Error saving file: %s", e.getMessage()));
152 }
153 }
154
155 public void createNewIdentity() {
156 IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
157 int registrationId = KeyHelper.generateRegistrationId(false);
158 axolotlStore = new JsonAxolotlStore(identityKey, registrationId);
159 groupStore = new JsonGroupStore();
160 registered = false;
161 }
162
163 public boolean isRegistered() {
164 return registered;
165 }
166
167 public void register(boolean voiceVerication) throws IOException {
168 password = Util.getSecret(18);
169
170 accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
171
172 if (voiceVerication)
173 accountManager.requestVoiceVerificationCode();
174 else
175 accountManager.requestSmsVerificationCode();
176
177 registered = false;
178 }
179
180 private static final int BATCH_SIZE = 100;
181
182 private List<PreKeyRecord> generatePreKeys() {
183 List<PreKeyRecord> records = new LinkedList<>();
184
185 for (int i = 0; i < BATCH_SIZE; i++) {
186 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
187 ECKeyPair keyPair = Curve.generateKeyPair();
188 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
189
190 axolotlStore.storePreKey(preKeyId, record);
191 records.add(record);
192 }
193
194 preKeyIdOffset = (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE;
195 return records;
196 }
197
198 private PreKeyRecord generateLastResortPreKey() {
199 if (axolotlStore.containsPreKey(Medium.MAX_VALUE)) {
200 try {
201 return axolotlStore.loadPreKey(Medium.MAX_VALUE);
202 } catch (InvalidKeyIdException e) {
203 axolotlStore.removePreKey(Medium.MAX_VALUE);
204 }
205 }
206
207 ECKeyPair keyPair = Curve.generateKeyPair();
208 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
209
210 axolotlStore.storePreKey(Medium.MAX_VALUE, record);
211
212 return record;
213 }
214
215 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
216 try {
217 ECKeyPair keyPair = Curve.generateKeyPair();
218 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
219 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
220
221 axolotlStore.storeSignedPreKey(nextSignedPreKeyId, record);
222 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
223
224 return record;
225 } catch (InvalidKeyException e) {
226 throw new AssertionError(e);
227 }
228 }
229
230 public void verifyAccount(String verificationCode) throws IOException {
231 verificationCode = verificationCode.replace("-", "");
232 signalingKey = Util.getSecret(52);
233 accountManager.verifyAccountWithCode(verificationCode, signalingKey, axolotlStore.getLocalRegistrationId(), false);
234
235 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
236 registered = true;
237
238 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
239
240 PreKeyRecord lastResortKey = generateLastResortPreKey();
241
242 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(axolotlStore.getIdentityKeyPair());
243
244 accountManager.setPreKeys(axolotlStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
245 }
246
247 public void sendMessage(List<String> recipients, TextSecureDataMessage message)
248 throws IOException, EncapsulatedExceptions {
249 TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, username, password,
250 axolotlStore, USER_AGENT, Optional.<TextSecureMessageSender.EventListener>absent());
251
252 Set<TextSecureAddress> recipientsTS = new HashSet<>(recipients.size());
253 for (String recipient : recipients) {
254 try {
255 recipientsTS.add(getPushAddress(recipient));
256 } catch (InvalidNumberException e) {
257 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
258 System.err.println("Aborting sending.");
259 return;
260 }
261 }
262
263 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
264
265 if (message.isEndSession()) {
266 for (TextSecureAddress recipient : recipientsTS) {
267 handleEndSession(recipient.getNumber());
268 }
269 }
270 }
271
272 private TextSecureContent decryptMessage(TextSecureEnvelope envelope) {
273 TextSecureCipher cipher = new TextSecureCipher(new TextSecureAddress(username), axolotlStore);
274 try {
275 return cipher.decrypt(envelope);
276 } catch (Exception e) {
277 // TODO handle all exceptions
278 e.printStackTrace();
279 return null;
280 }
281 }
282
283 private void handleEndSession(String source) {
284 axolotlStore.deleteAllSessions(source);
285 }
286
287 public interface ReceiveMessageHandler {
288 void handleMessage(TextSecureEnvelope envelope, TextSecureContent decryptedContent, GroupInfo group);
289 }
290
291 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
292 final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
293 TextSecureMessagePipe messagePipe = null;
294
295 try {
296 messagePipe = messageReceiver.createMessagePipe();
297
298 while (true) {
299 TextSecureEnvelope envelope;
300 TextSecureContent content = null;
301 GroupInfo group = null;
302 try {
303 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
304 if (!envelope.isReceipt()) {
305 content = decryptMessage(envelope);
306 if (content != null) {
307 if (content.getDataMessage().isPresent()) {
308 TextSecureDataMessage message = content.getDataMessage().get();
309 if (message.getGroupInfo().isPresent()) {
310 TextSecureGroup groupInfo = message.getGroupInfo().get();
311 switch (groupInfo.getType()) {
312 case UPDATE:
313 group = groupStore.getGroup(groupInfo.getGroupId());
314 if (group == null) {
315 group = new GroupInfo(groupInfo.getGroupId());
316 }
317
318 if (groupInfo.getAvatar().isPresent()) {
319 TextSecureAttachment avatar = groupInfo.getAvatar().get();
320 if (avatar.isPointer()) {
321 long avatarId = avatar.asPointer().getId();
322 try {
323 retrieveAttachment(avatar.asPointer());
324 group.avatarId = avatarId;
325 } catch (IOException | InvalidMessageException e) {
326 System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
327 }
328 }
329 }
330
331 if (groupInfo.getName().isPresent()) {
332 group.name = groupInfo.getName().get();
333 }
334
335 if (groupInfo.getMembers().isPresent()) {
336 group.members.addAll(groupInfo.getMembers().get());
337 }
338
339 groupStore.updateGroup(group);
340 break;
341 case DELIVER:
342 group = groupStore.getGroup(groupInfo.getGroupId());
343 break;
344 case QUIT:
345 group = groupStore.getGroup(groupInfo.getGroupId());
346 if (group != null) {
347 group.members.remove(envelope.getSource());
348 }
349 break;
350 }
351 }
352 if (message.isEndSession()) {
353 handleEndSession(envelope.getSource());
354 }
355 if (message.getAttachments().isPresent()) {
356 for (TextSecureAttachment attachment : message.getAttachments().get()) {
357 if (attachment.isPointer()) {
358 try {
359 retrieveAttachment(attachment.asPointer());
360 } catch (IOException | InvalidMessageException e) {
361 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
362 }
363 }
364 }
365 }
366 }
367 }
368 }
369 handler.handleMessage(envelope, content, group);
370 } catch (TimeoutException e) {
371 if (returnOnTimeout)
372 return;
373 } catch (InvalidVersionException e) {
374 System.err.println("Ignoring error: " + e.getMessage());
375 }
376 save();
377 }
378 } finally {
379 if (messagePipe != null)
380 messagePipe.shutdown();
381 }
382 }
383
384 public File getAttachmentFile(long attachmentId) {
385 return new File(attachmentsPath + "/" + attachmentId);
386 }
387
388 private File retrieveAttachment(TextSecureAttachmentPointer pointer) throws IOException, InvalidMessageException {
389 final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
390
391 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
392 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
393
394 new File(attachmentsPath).mkdirs();
395 File outputFile = getAttachmentFile(pointer.getId());
396 OutputStream output = null;
397 try {
398 output = new FileOutputStream(outputFile);
399 byte[] buffer = new byte[4096];
400 int read;
401
402 while ((read = input.read(buffer)) != -1) {
403 output.write(buffer, 0, read);
404 }
405 } catch (FileNotFoundException e) {
406 e.printStackTrace();
407 return null;
408 } finally {
409 if (output != null) {
410 output.close();
411 output = null;
412 }
413 if (!tmpFile.delete()) {
414 System.err.println("Failed to delete temp file: " + tmpFile);
415 }
416 }
417 if (pointer.getPreview().isPresent()) {
418 File previewFile = new File(outputFile + ".preview");
419 try {
420 output = new FileOutputStream(previewFile);
421 byte[] preview = pointer.getPreview().get();
422 output.write(preview, 0, preview.length);
423 } catch (FileNotFoundException e) {
424 e.printStackTrace();
425 return null;
426 } finally {
427 if (output != null) {
428 output.close();
429 }
430 }
431 }
432 return outputFile;
433 }
434
435 public String canonicalizeNumber(String number) throws InvalidNumberException {
436 String localNumber = username;
437 return PhoneNumberFormatter.formatNumber(number, localNumber);
438 }
439
440 private TextSecureAddress getPushAddress(String number) throws InvalidNumberException {
441 String e164number = canonicalizeNumber(number);
442 return new TextSecureAddress(e164number);
443 }
444
445 public GroupInfo getGroupInfo(byte[] groupId) {
446 return groupStore.getGroup(groupId);
447 }
448
449 public void setGroupInfo(GroupInfo group) {
450 groupStore.updateGroup(group);
451 }
452
453 public String getUsername() {
454 return username;
455 }
456 }