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