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