]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/textsecure/Manager.java
Implement fetch messages
[signal-cli] / src / main / java / org / asamk / textsecure / 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 org.asamk.textsecure;
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.asamk.TextSecure;
27 import org.whispersystems.libaxolotl.*;
28 import org.whispersystems.libaxolotl.ecc.Curve;
29 import org.whispersystems.libaxolotl.ecc.ECKeyPair;
30 import org.whispersystems.libaxolotl.state.PreKeyRecord;
31 import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
32 import org.whispersystems.libaxolotl.util.KeyHelper;
33 import org.whispersystems.libaxolotl.util.Medium;
34 import org.whispersystems.libaxolotl.util.guava.Optional;
35 import org.whispersystems.textsecure.api.TextSecureAccountManager;
36 import org.whispersystems.textsecure.api.TextSecureMessagePipe;
37 import org.whispersystems.textsecure.api.TextSecureMessageReceiver;
38 import org.whispersystems.textsecure.api.TextSecureMessageSender;
39 import org.whispersystems.textsecure.api.crypto.TextSecureCipher;
40 import org.whispersystems.textsecure.api.messages.*;
41 import org.whispersystems.textsecure.api.push.TextSecureAddress;
42 import org.whispersystems.textsecure.api.push.TrustStore;
43 import org.whispersystems.textsecure.api.push.exceptions.EncapsulatedExceptions;
44 import org.whispersystems.textsecure.api.util.InvalidNumberException;
45 import org.whispersystems.textsecure.api.util.PhoneNumberFormatter;
46
47 import java.io.*;
48 import java.nio.file.Files;
49 import java.nio.file.Paths;
50 import java.util.*;
51 import java.util.concurrent.TimeUnit;
52 import java.util.concurrent.TimeoutException;
53
54 class Manager implements TextSecure {
55 private final static String URL = "https://textsecure-service.whispersystems.org";
56 private final static TrustStore TRUST_STORE = new WhisperTrustStore();
57
58 public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
59 public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
60 private final static String USER_AGENT = PROJECT_NAME + " " + PROJECT_VERSION;
61
62 private final static String settingsPath = System.getProperty("user.home") + "/.config/textsecure";
63 private final static String dataPath = settingsPath + "/data";
64 private final static String attachmentsPath = settingsPath + "/attachments";
65
66 private final ObjectMapper jsonProcessot = new ObjectMapper();
67 private String username;
68 private String password;
69 private String signalingKey;
70 private int preKeyIdOffset;
71 private int nextSignedPreKeyId;
72
73 private boolean registered = false;
74
75 private JsonAxolotlStore axolotlStore;
76 private TextSecureAccountManager accountManager;
77 private JsonGroupStore groupStore;
78
79 public Manager(String username) {
80 this.username = username;
81 jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
82 jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
83 jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
84 jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
85 }
86
87 public String getFileName() {
88 new File(dataPath).mkdirs();
89 return dataPath + "/" + username;
90 }
91
92 public boolean userExists() {
93 File f = new File(getFileName());
94 return !(!f.exists() || f.isDirectory());
95 }
96
97 public boolean userHasKeys() {
98 return axolotlStore != null;
99 }
100
101 private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
102 JsonNode node = parent.get(name);
103 if (node == null) {
104 throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
105 }
106
107 return node;
108 }
109
110 public void load() throws IOException, InvalidKeyException {
111 JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
112
113 username = getNotNullNode(rootNode, "username").asText();
114 password = getNotNullNode(rootNode, "password").asText();
115 if (rootNode.has("signalingKey")) {
116 signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
117 }
118 if (rootNode.has("preKeyIdOffset")) {
119 preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
120 } else {
121 preKeyIdOffset = 0;
122 }
123 if (rootNode.has("nextSignedPreKeyId")) {
124 nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
125 } else {
126 nextSignedPreKeyId = 0;
127 }
128 axolotlStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonAxolotlStore.class); //new JsonAxolotlStore(in.getJSONObject("axolotlStore"));
129 registered = getNotNullNode(rootNode, "registered").asBoolean();
130 JsonNode groupStoreNode = rootNode.get("groupStore");
131 if (groupStoreNode != null) {
132 groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class);
133 }
134 if (groupStore == null) {
135 groupStore = new JsonGroupStore();
136 }
137 accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
138 }
139
140 private void save() {
141 ObjectNode rootNode = jsonProcessot.createObjectNode();
142 rootNode.put("username", username)
143 .put("password", password)
144 .put("signalingKey", signalingKey)
145 .put("preKeyIdOffset", preKeyIdOffset)
146 .put("nextSignedPreKeyId", nextSignedPreKeyId)
147 .put("registered", registered)
148 .putPOJO("axolotlStore", axolotlStore)
149 .putPOJO("groupStore", groupStore)
150 ;
151 try {
152 jsonProcessot.writeValue(new File(getFileName()), rootNode);
153 } catch (Exception e) {
154 System.err.println(String.format("Error saving file: %s", e.getMessage()));
155 }
156 }
157
158 public void createNewIdentity() {
159 IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
160 int registrationId = KeyHelper.generateRegistrationId(false);
161 axolotlStore = new JsonAxolotlStore(identityKey, registrationId);
162 groupStore = new JsonGroupStore();
163 registered = false;
164 save();
165 }
166
167 public boolean isRegistered() {
168 return registered;
169 }
170
171 public void register(boolean voiceVerication) throws IOException {
172 password = Util.getSecret(18);
173
174 accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
175
176 if (voiceVerication)
177 accountManager.requestVoiceVerificationCode();
178 else
179 accountManager.requestSmsVerificationCode();
180
181 registered = false;
182 save();
183 }
184
185 private static final int BATCH_SIZE = 100;
186
187 private List<PreKeyRecord> generatePreKeys() {
188 List<PreKeyRecord> records = new LinkedList<>();
189
190 for (int i = 0; i < BATCH_SIZE; i++) {
191 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
192 ECKeyPair keyPair = Curve.generateKeyPair();
193 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
194
195 axolotlStore.storePreKey(preKeyId, record);
196 records.add(record);
197 }
198
199 preKeyIdOffset = (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE;
200 save();
201
202 return records;
203 }
204
205 private PreKeyRecord generateLastResortPreKey() {
206 if (axolotlStore.containsPreKey(Medium.MAX_VALUE)) {
207 try {
208 return axolotlStore.loadPreKey(Medium.MAX_VALUE);
209 } catch (InvalidKeyIdException e) {
210 axolotlStore.removePreKey(Medium.MAX_VALUE);
211 }
212 }
213
214 ECKeyPair keyPair = Curve.generateKeyPair();
215 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
216
217 axolotlStore.storePreKey(Medium.MAX_VALUE, record);
218 save();
219
220 return record;
221 }
222
223 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
224 try {
225 ECKeyPair keyPair = Curve.generateKeyPair();
226 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
227 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
228
229 axolotlStore.storeSignedPreKey(nextSignedPreKeyId, record);
230 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
231 save();
232
233 return record;
234 } catch (InvalidKeyException e) {
235 throw new AssertionError(e);
236 }
237 }
238
239 public void verifyAccount(String verificationCode) throws IOException {
240 verificationCode = verificationCode.replace("-", "");
241 signalingKey = Util.getSecret(52);
242 accountManager.verifyAccountWithCode(verificationCode, signalingKey, axolotlStore.getLocalRegistrationId(), false, true);
243
244 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
245 registered = true;
246
247 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
248
249 PreKeyRecord lastResortKey = generateLastResortPreKey();
250
251 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(axolotlStore.getIdentityKeyPair());
252
253 accountManager.setPreKeys(axolotlStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
254 save();
255 }
256
257
258 private static List<TextSecureAttachment> getTextSecureAttachments(List<String> attachments) throws AttachmentInvalidException {
259 List<TextSecureAttachment> textSecureAttachments = null;
260 if (attachments != null) {
261 textSecureAttachments = new ArrayList<>(attachments.size());
262 for (String attachment : attachments) {
263 try {
264 textSecureAttachments.add(createAttachment(attachment));
265 } catch (IOException e) {
266 throw new AttachmentInvalidException(attachment, e);
267 }
268 }
269 }
270 return textSecureAttachments;
271 }
272
273 private static TextSecureAttachmentStream createAttachment(String attachment) throws IOException {
274 File attachmentFile = new File(attachment);
275 InputStream attachmentStream = new FileInputStream(attachmentFile);
276 final long attachmentSize = attachmentFile.length();
277 String mime = Files.probeContentType(Paths.get(attachment));
278 return new TextSecureAttachmentStream(attachmentStream, mime, attachmentSize, null);
279 }
280
281 @Override
282 public void sendGroupMessage(String messageText, List<String> attachments,
283 byte[] groupId)
284 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
285 final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText);
286 if (attachments != null) {
287 messageBuilder.withAttachments(getTextSecureAttachments(attachments));
288 }
289 if (groupId != null) {
290 TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.DELIVER)
291 .withId(groupId)
292 .build();
293 messageBuilder.asGroupMessage(group);
294 }
295 TextSecureDataMessage message = messageBuilder.build();
296
297 sendMessage(message, groupStore.getGroup(groupId).members);
298 }
299
300 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
301 TextSecureGroup group = TextSecureGroup.newBuilder(TextSecureGroup.Type.QUIT)
302 .withId(groupId)
303 .build();
304
305 TextSecureDataMessage message = TextSecureDataMessage.newBuilder()
306 .asGroupMessage(group)
307 .build();
308
309 sendMessage(message, groupStore.getGroup(groupId).members);
310 }
311
312 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
313 GroupInfo g;
314 if (groupId == null) {
315 // Create new group
316 g = new GroupInfo(Util.getSecretBytes(16));
317 g.members.add(username);
318 } else {
319 g = groupStore.getGroup(groupId);
320 }
321
322 if (name != null) {
323 g.name = name;
324 }
325
326 if (members != null) {
327 for (String member : members) {
328 try {
329 g.members.add(canonicalizeNumber(member));
330 } catch (InvalidNumberException e) {
331 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
332 System.err.println("Aborting…");
333 System.exit(1);
334 }
335 }
336 }
337
338 TextSecureGroup.Builder group = TextSecureGroup.newBuilder(TextSecureGroup.Type.UPDATE)
339 .withId(g.groupId)
340 .withName(g.name)
341 .withMembers(new ArrayList<>(g.members));
342
343 if (avatarFile != null) {
344 try {
345 group.withAvatar(createAttachment(avatarFile));
346 // TODO
347 g.avatarId = 0;
348 } catch (IOException e) {
349 throw new AttachmentInvalidException(avatarFile, e);
350 }
351 }
352
353 groupStore.updateGroup(g);
354
355 TextSecureDataMessage message = TextSecureDataMessage.newBuilder()
356 .asGroupMessage(group.build())
357 .build();
358
359 sendMessage(message, g.members);
360 return g.groupId;
361 }
362
363 @Override
364 public void sendMessage(String message, List<String> attachments, String recipient)
365 throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
366 List<String> recipients = new ArrayList<>(1);
367 recipients.add(recipient);
368 sendMessage(message, attachments, recipients);
369 }
370
371 @Override
372 public void sendMessage(String messageText, List<String> attachments,
373 List<String> recipients)
374 throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
375 final TextSecureDataMessage.Builder messageBuilder = TextSecureDataMessage.newBuilder().withBody(messageText);
376 if (attachments != null) {
377 messageBuilder.withAttachments(getTextSecureAttachments(attachments));
378 }
379 TextSecureDataMessage message = messageBuilder.build();
380
381 sendMessage(message, recipients);
382 }
383
384 @Override
385 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
386 TextSecureDataMessage message = TextSecureDataMessage.newBuilder()
387 .asEndSessionMessage()
388 .build();
389
390 sendMessage(message, recipients);
391 }
392
393 private void sendMessage(TextSecureDataMessage message, Collection<String> recipients)
394 throws IOException, EncapsulatedExceptions {
395 TextSecureMessageSender messageSender = new TextSecureMessageSender(URL, TRUST_STORE, username, password,
396 axolotlStore, USER_AGENT, Optional.<TextSecureMessageSender.EventListener>absent());
397
398 Set<TextSecureAddress> recipientsTS = new HashSet<>(recipients.size());
399 for (String recipient : recipients) {
400 try {
401 recipientsTS.add(getPushAddress(recipient));
402 } catch (InvalidNumberException e) {
403 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
404 System.err.println("Aborting sending.");
405 save();
406 return;
407 }
408 }
409
410 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
411
412 if (message.isEndSession()) {
413 for (TextSecureAddress recipient : recipientsTS) {
414 handleEndSession(recipient.getNumber());
415 }
416 }
417 save();
418 }
419
420 private TextSecureContent decryptMessage(TextSecureEnvelope envelope) {
421 TextSecureCipher cipher = new TextSecureCipher(new TextSecureAddress(username), axolotlStore);
422 try {
423 return cipher.decrypt(envelope);
424 } catch (Exception e) {
425 // TODO handle all exceptions
426 e.printStackTrace();
427 return null;
428 }
429 }
430
431 private void handleEndSession(String source) {
432 axolotlStore.deleteAllSessions(source);
433 }
434
435 public interface ReceiveMessageHandler {
436 void handleMessage(TextSecureEnvelope envelope, TextSecureContent decryptedContent, GroupInfo group);
437 }
438
439 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
440 final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
441 TextSecureMessagePipe messagePipe = null;
442
443 try {
444 messagePipe = messageReceiver.createMessagePipe();
445
446 while (true) {
447 TextSecureEnvelope envelope;
448 TextSecureContent content = null;
449 GroupInfo group = null;
450 try {
451 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
452 if (!envelope.isReceipt()) {
453 content = decryptMessage(envelope);
454 if (content != null) {
455 if (content.getDataMessage().isPresent()) {
456 TextSecureDataMessage message = content.getDataMessage().get();
457 if (message.getGroupInfo().isPresent()) {
458 TextSecureGroup groupInfo = message.getGroupInfo().get();
459 switch (groupInfo.getType()) {
460 case UPDATE:
461 try {
462 group = groupStore.getGroup(groupInfo.getGroupId());
463 } catch (GroupNotFoundException e) {
464 group = new GroupInfo(groupInfo.getGroupId());
465 }
466
467 if (groupInfo.getAvatar().isPresent()) {
468 TextSecureAttachment avatar = groupInfo.getAvatar().get();
469 if (avatar.isPointer()) {
470 long avatarId = avatar.asPointer().getId();
471 try {
472 retrieveAttachment(avatar.asPointer());
473 group.avatarId = avatarId;
474 } catch (IOException | InvalidMessageException e) {
475 System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
476 }
477 }
478 }
479
480 if (groupInfo.getName().isPresent()) {
481 group.name = groupInfo.getName().get();
482 }
483
484 if (groupInfo.getMembers().isPresent()) {
485 group.members.addAll(groupInfo.getMembers().get());
486 }
487
488 groupStore.updateGroup(group);
489 break;
490 case DELIVER:
491 try {
492 group = groupStore.getGroup(groupInfo.getGroupId());
493 } catch (GroupNotFoundException e) {
494 }
495 break;
496 case QUIT:
497 try {
498 group = groupStore.getGroup(groupInfo.getGroupId());
499 group.members.remove(envelope.getSource());
500 } catch (GroupNotFoundException e) {
501 }
502 break;
503 }
504 }
505 if (message.isEndSession()) {
506 handleEndSession(envelope.getSource());
507 }
508 if (message.getAttachments().isPresent()) {
509 for (TextSecureAttachment attachment : message.getAttachments().get()) {
510 if (attachment.isPointer()) {
511 try {
512 retrieveAttachment(attachment.asPointer());
513 } catch (IOException | InvalidMessageException e) {
514 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
515 }
516 }
517 }
518 }
519 }
520 }
521 }
522 save();
523 handler.handleMessage(envelope, content, group);
524 } catch (TimeoutException e) {
525 if (returnOnTimeout)
526 return;
527 } catch (InvalidVersionException e) {
528 System.err.println("Ignoring error: " + e.getMessage());
529 }
530 }
531 } finally {
532 if (messagePipe != null)
533 messagePipe.shutdown();
534 }
535 }
536
537 public File getAttachmentFile(long attachmentId) {
538 return new File(attachmentsPath, attachmentId + "");
539 }
540
541 private File retrieveAttachment(TextSecureAttachmentPointer pointer) throws IOException, InvalidMessageException {
542 final TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
543
544 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
545 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
546
547 new File(attachmentsPath).mkdirs();
548 File outputFile = getAttachmentFile(pointer.getId());
549 OutputStream output = null;
550 try {
551 output = new FileOutputStream(outputFile);
552 byte[] buffer = new byte[4096];
553 int read;
554
555 while ((read = input.read(buffer)) != -1) {
556 output.write(buffer, 0, read);
557 }
558 } catch (FileNotFoundException e) {
559 e.printStackTrace();
560 return null;
561 } finally {
562 if (output != null) {
563 output.close();
564 output = null;
565 }
566 if (!tmpFile.delete()) {
567 System.err.println("Failed to delete temp file: " + tmpFile);
568 }
569 }
570 if (pointer.getPreview().isPresent()) {
571 File previewFile = new File(outputFile + ".preview");
572 try {
573 output = new FileOutputStream(previewFile);
574 byte[] preview = pointer.getPreview().get();
575 output.write(preview, 0, preview.length);
576 } catch (FileNotFoundException e) {
577 e.printStackTrace();
578 return null;
579 } finally {
580 if (output != null) {
581 output.close();
582 }
583 }
584 }
585 return outputFile;
586 }
587
588 private String canonicalizeNumber(String number) throws InvalidNumberException {
589 String localNumber = username;
590 return PhoneNumberFormatter.formatNumber(number, localNumber);
591 }
592
593 private TextSecureAddress getPushAddress(String number) throws InvalidNumberException {
594 String e164number = canonicalizeNumber(number);
595 return new TextSecureAddress(e164number);
596 }
597
598 @Override
599 public boolean isRemote() {
600 return false;
601 }
602 }