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