]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
1b460a77890a8259f0a02ad70cc20454a3822b78
[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.apache.http.util.TextUtils;
27 import org.asamk.Signal;
28 import org.whispersystems.libsignal.*;
29 import org.whispersystems.libsignal.ecc.Curve;
30 import org.whispersystems.libsignal.ecc.ECKeyPair;
31 import org.whispersystems.libsignal.ecc.ECPublicKey;
32 import org.whispersystems.libsignal.state.PreKeyRecord;
33 import org.whispersystems.libsignal.state.SignalProtocolStore;
34 import org.whispersystems.libsignal.state.SignedPreKeyRecord;
35 import org.whispersystems.libsignal.util.KeyHelper;
36 import org.whispersystems.libsignal.util.Medium;
37 import org.whispersystems.libsignal.util.guava.Optional;
38 import org.whispersystems.signalservice.api.SignalServiceAccountManager;
39 import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
40 import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
41 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
42 import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
43 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
44 import org.whispersystems.signalservice.api.messages.*;
45 import org.whispersystems.signalservice.api.messages.multidevice.*;
46 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
47 import org.whispersystems.signalservice.api.push.TrustStore;
48 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
49 import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
50 import org.whispersystems.signalservice.api.util.InvalidNumberException;
51 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
52 import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
53
54 import java.io.*;
55 import java.net.URI;
56 import java.net.URISyntaxException;
57 import java.net.URLDecoder;
58 import java.net.URLEncoder;
59 import java.nio.file.Files;
60 import java.nio.file.Paths;
61 import java.util.*;
62 import java.util.concurrent.TimeUnit;
63 import java.util.concurrent.TimeoutException;
64
65 class Manager implements Signal {
66 private final static String URL = "https://textsecure-service.whispersystems.org";
67 private final static TrustStore TRUST_STORE = new WhisperTrustStore();
68
69 public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
70 public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
71 private final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION;
72
73 private final static int PREKEY_MINIMUM_COUNT = 20;
74 private static final int PREKEY_BATCH_SIZE = 100;
75
76 private final String settingsPath;
77 private final String dataPath;
78 private final String attachmentsPath;
79
80 private final ObjectMapper jsonProcessot = new ObjectMapper();
81 private String username;
82 private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
83 private String password;
84 private String signalingKey;
85 private int preKeyIdOffset;
86 private int nextSignedPreKeyId;
87
88 private boolean registered = false;
89
90 private SignalProtocolStore signalProtocolStore;
91 private SignalServiceAccountManager accountManager;
92 private JsonGroupStore groupStore;
93 private JsonContactsStore contactStore;
94
95 public Manager(String username, String settingsPath) {
96 this.username = username;
97 this.settingsPath = settingsPath;
98 this.dataPath = this.settingsPath + "/data";
99 this.attachmentsPath = this.settingsPath + "/attachments";
100
101 jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
102 jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
103 jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
104 jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
105 }
106
107 public String getUsername() {
108 return username;
109 }
110
111 public int getDeviceId() {
112 return deviceId;
113 }
114
115 public String getFileName() {
116 new File(dataPath).mkdirs();
117 return dataPath + "/" + username;
118 }
119
120 public boolean userExists() {
121 if (username == null) {
122 return false;
123 }
124 File f = new File(getFileName());
125 return !(!f.exists() || f.isDirectory());
126 }
127
128 public boolean userHasKeys() {
129 return signalProtocolStore != null;
130 }
131
132 private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
133 JsonNode node = parent.get(name);
134 if (node == null) {
135 throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
136 }
137
138 return node;
139 }
140
141 public void load() throws IOException, InvalidKeyException {
142 JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
143
144 JsonNode node = rootNode.get("deviceId");
145 if (node != null) {
146 deviceId = node.asInt();
147 }
148 username = getNotNullNode(rootNode, "username").asText();
149 password = getNotNullNode(rootNode, "password").asText();
150 if (rootNode.has("signalingKey")) {
151 signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
152 }
153 if (rootNode.has("preKeyIdOffset")) {
154 preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
155 } else {
156 preKeyIdOffset = 0;
157 }
158 if (rootNode.has("nextSignedPreKeyId")) {
159 nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
160 } else {
161 nextSignedPreKeyId = 0;
162 }
163 signalProtocolStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
164 registered = getNotNullNode(rootNode, "registered").asBoolean();
165 JsonNode groupStoreNode = rootNode.get("groupStore");
166 if (groupStoreNode != null) {
167 groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class);
168 }
169 if (groupStore == null) {
170 groupStore = new JsonGroupStore();
171 }
172 JsonNode contactStoreNode = rootNode.get("contactStore");
173 if (contactStoreNode != null) {
174 contactStore = jsonProcessot.convertValue(contactStoreNode, JsonContactsStore.class);
175 }
176 if (contactStore == null) {
177 contactStore = new JsonContactsStore();
178 }
179
180 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT);
181 try {
182 if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) {
183 refreshPreKeys();
184 save();
185 }
186 } catch (AuthorizationFailedException e) {
187 System.err.println("Authorization failed, was the number registered elsewhere?");
188 }
189 }
190
191 private void save() {
192 if (username == null) {
193 return;
194 }
195 ObjectNode rootNode = jsonProcessot.createObjectNode();
196 rootNode.put("username", username)
197 .put("deviceId", deviceId)
198 .put("password", password)
199 .put("signalingKey", signalingKey)
200 .put("preKeyIdOffset", preKeyIdOffset)
201 .put("nextSignedPreKeyId", nextSignedPreKeyId)
202 .put("registered", registered)
203 .putPOJO("axolotlStore", signalProtocolStore)
204 .putPOJO("groupStore", groupStore)
205 .putPOJO("contactStore", contactStore)
206 ;
207 try {
208 jsonProcessot.writeValue(new File(getFileName()), rootNode);
209 } catch (Exception e) {
210 System.err.println(String.format("Error saving file: %s", e.getMessage()));
211 }
212 }
213
214 public void createNewIdentity() {
215 IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
216 int registrationId = KeyHelper.generateRegistrationId(false);
217 signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
218 groupStore = new JsonGroupStore();
219 registered = false;
220 save();
221 }
222
223 public boolean isRegistered() {
224 return registered;
225 }
226
227 public void register(boolean voiceVerication) throws IOException {
228 password = Util.getSecret(18);
229
230 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
231
232 if (voiceVerication)
233 accountManager.requestVoiceVerificationCode();
234 else
235 accountManager.requestSmsVerificationCode();
236
237 registered = false;
238 save();
239 }
240
241 public URI getDeviceLinkUri() throws TimeoutException, IOException {
242 password = Util.getSecret(18);
243
244 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
245 String uuid = accountManager.getNewDeviceUuid();
246
247 registered = false;
248 try {
249 return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8"));
250 } catch (URISyntaxException e) {
251 // Shouldn't happen
252 return null;
253 }
254 }
255
256 public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
257 signalingKey = Util.getSecret(52);
258 SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName);
259 deviceId = ret.getDeviceId();
260 username = ret.getNumber();
261 // TODO do this check before actually registering
262 if (userExists()) {
263 throw new UserAlreadyExists(username, getFileName());
264 }
265 signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId());
266
267 registered = true;
268 refreshPreKeys();
269
270 requestSyncGroups();
271 requestSyncContacts();
272
273 save();
274 }
275
276 public List<DeviceInfo> getLinkedDevices() throws IOException {
277 return accountManager.getDevices();
278 }
279
280 public void removeLinkedDevices(int deviceId) throws IOException {
281 accountManager.removeDevice(deviceId);
282 }
283
284 public static Map<String, String> getQueryMap(String query) {
285 String[] params = query.split("&");
286 Map<String, String> map = new HashMap<>();
287 for (String param : params) {
288 String name = null;
289 try {
290 name = URLDecoder.decode(param.split("=")[0], "utf-8");
291 } catch (UnsupportedEncodingException e) {
292 // Impossible
293 }
294 String value = null;
295 try {
296 value = URLDecoder.decode(param.split("=")[1], "utf-8");
297 } catch (UnsupportedEncodingException e) {
298 // Impossible
299 }
300 map.put(name, value);
301 }
302 return map;
303 }
304
305 public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
306 Map<String, String> query = getQueryMap(linkUri.getRawQuery());
307 String deviceIdentifier = query.get("uuid");
308 String publicKeyEncoded = query.get("pub_key");
309
310 if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) {
311 throw new RuntimeException("Invalid device link uri");
312 }
313
314 ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
315
316 addDevice(deviceIdentifier, deviceKey);
317 }
318
319 private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
320 IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair();
321 String verificationCode = accountManager.getNewDeviceVerificationCode();
322
323 accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, verificationCode);
324 }
325
326 private List<PreKeyRecord> generatePreKeys() {
327 List<PreKeyRecord> records = new LinkedList<>();
328
329 for (int i = 0; i < PREKEY_BATCH_SIZE; i++) {
330 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
331 ECKeyPair keyPair = Curve.generateKeyPair();
332 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
333
334 signalProtocolStore.storePreKey(preKeyId, record);
335 records.add(record);
336 }
337
338 preKeyIdOffset = (preKeyIdOffset + PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE;
339 save();
340
341 return records;
342 }
343
344 private PreKeyRecord getOrGenerateLastResortPreKey() {
345 if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) {
346 try {
347 return signalProtocolStore.loadPreKey(Medium.MAX_VALUE);
348 } catch (InvalidKeyIdException e) {
349 signalProtocolStore.removePreKey(Medium.MAX_VALUE);
350 }
351 }
352
353 ECKeyPair keyPair = Curve.generateKeyPair();
354 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
355
356 signalProtocolStore.storePreKey(Medium.MAX_VALUE, record);
357 save();
358
359 return record;
360 }
361
362 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
363 try {
364 ECKeyPair keyPair = Curve.generateKeyPair();
365 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
366 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
367
368 signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record);
369 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
370 save();
371
372 return record;
373 } catch (InvalidKeyException e) {
374 throw new AssertionError(e);
375 }
376 }
377
378 public void verifyAccount(String verificationCode) throws IOException {
379 verificationCode = verificationCode.replace("-", "");
380 signalingKey = Util.getSecret(52);
381 accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, true);
382
383 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
384 registered = true;
385
386 refreshPreKeys();
387 save();
388 }
389
390 private void refreshPreKeys() throws IOException {
391 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
392 PreKeyRecord lastResortKey = getOrGenerateLastResortPreKey();
393 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair());
394
395 accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
396 }
397
398
399 private static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
400 List<SignalServiceAttachment> SignalServiceAttachments = null;
401 if (attachments != null) {
402 SignalServiceAttachments = new ArrayList<>(attachments.size());
403 for (String attachment : attachments) {
404 try {
405 SignalServiceAttachments.add(createAttachment(attachment));
406 } catch (IOException e) {
407 throw new AttachmentInvalidException(attachment, e);
408 }
409 }
410 }
411 return SignalServiceAttachments;
412 }
413
414 private static SignalServiceAttachment createAttachment(String attachment) throws IOException {
415 File attachmentFile = new File(attachment);
416 InputStream attachmentStream = new FileInputStream(attachmentFile);
417 final long attachmentSize = attachmentFile.length();
418 String mime = Files.probeContentType(Paths.get(attachment));
419 return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null);
420 }
421
422 @Override
423 public void sendGroupMessage(String messageText, List<String> attachments,
424 byte[] groupId)
425 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException {
426 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
427 if (attachments != null) {
428 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
429 }
430 if (groupId != null) {
431 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
432 .withId(groupId)
433 .build();
434 messageBuilder.asGroupMessage(group);
435 }
436 SignalServiceDataMessage message = messageBuilder.build();
437
438 GroupInfo g = groupStore.getGroup(groupId);
439 if (g == null) {
440 throw new GroupNotFoundException(groupId);
441 }
442 Set<String> members = g.members;
443 members.remove(this.username);
444 sendMessage(message, members);
445 }
446
447 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException {
448 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
449 .withId(groupId)
450 .build();
451
452 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
453 .asGroupMessage(group)
454 .build();
455
456 final GroupInfo g = groupStore.getGroup(groupId);
457 if (g == null) {
458 throw new GroupNotFoundException(groupId);
459 }
460 g.members.remove(this.username);
461 groupStore.updateGroup(g);
462
463 sendMessage(message, g.members);
464 }
465
466 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException {
467 GroupInfo g;
468 if (groupId == null) {
469 // Create new group
470 g = new GroupInfo(Util.getSecretBytes(16));
471 g.members.add(username);
472 } else {
473 g = groupStore.getGroup(groupId);
474 if (g == null) {
475 throw new GroupNotFoundException(groupId);
476 }
477 }
478
479 if (name != null) {
480 g.name = name;
481 }
482
483 if (members != null) {
484 for (String member : members) {
485 try {
486 g.members.add(canonicalizeNumber(member));
487 } catch (InvalidNumberException e) {
488 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
489 System.err.println("Aborting…");
490 System.exit(1);
491 }
492 }
493 }
494
495 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
496 .withId(g.groupId)
497 .withName(g.name)
498 .withMembers(new ArrayList<>(g.members));
499
500 if (avatarFile != null) {
501 try {
502 group.withAvatar(createAttachment(avatarFile));
503 // TODO
504 g.avatarId = 0;
505 } catch (IOException e) {
506 throw new AttachmentInvalidException(avatarFile, e);
507 }
508 }
509
510 groupStore.updateGroup(g);
511
512 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
513 .asGroupMessage(group.build())
514 .build();
515
516 final Set<String> membersSend = g.members;
517 membersSend.remove(this.username);
518 sendMessage(message, membersSend);
519 return g.groupId;
520 }
521
522 @Override
523 public void sendMessage(String message, List<String> attachments, String recipient)
524 throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException {
525 List<String> recipients = new ArrayList<>(1);
526 recipients.add(recipient);
527 sendMessage(message, attachments, recipients);
528 }
529
530 @Override
531 public void sendMessage(String messageText, List<String> attachments,
532 List<String> recipients)
533 throws IOException, EncapsulatedExceptions, AttachmentInvalidException, UntrustedIdentityException {
534 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
535 if (attachments != null) {
536 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
537 }
538 SignalServiceDataMessage message = messageBuilder.build();
539
540 sendMessage(message, recipients);
541 }
542
543 @Override
544 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
545 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
546 .asEndSessionMessage()
547 .build();
548
549 sendMessage(message, recipients);
550 }
551
552 private void requestSyncGroups() throws IOException {
553 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
554 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
555 try {
556 sendMessage(message);
557 } catch (EncapsulatedExceptions encapsulatedExceptions) {
558 encapsulatedExceptions.printStackTrace();
559 } catch (UntrustedIdentityException e) {
560 e.printStackTrace();
561 }
562 }
563
564 private void requestSyncContacts() throws IOException {
565 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
566 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
567 try {
568 sendMessage(message);
569 } catch (EncapsulatedExceptions encapsulatedExceptions) {
570 encapsulatedExceptions.printStackTrace();
571 } catch (UntrustedIdentityException e) {
572 e.printStackTrace();
573 }
574 }
575
576 private void sendMessage(SignalServiceSyncMessage message)
577 throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
578 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
579 deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
580 messageSender.sendMessage(message);
581 }
582
583 private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
584 throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
585 try {
586 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
587 deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
588
589 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
590 for (String recipient : recipients) {
591 try {
592 recipientsTS.add(getPushAddress(recipient));
593 } catch (InvalidNumberException e) {
594 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
595 System.err.println("Aborting sending.");
596 save();
597 return;
598 }
599 }
600
601 if (message.getGroupInfo().isPresent()) {
602 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
603 } else {
604 // Send to all individually, so sync messages are sent correctly
605 for (SignalServiceAddress address : recipientsTS) {
606 messageSender.sendMessage(address, message);
607 }
608 }
609
610 if (message.isEndSession()) {
611 for (SignalServiceAddress recipient : recipientsTS) {
612 handleEndSession(recipient.getNumber());
613 }
614 }
615 } finally {
616 save();
617 }
618 }
619
620 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) {
621 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
622 try {
623 return cipher.decrypt(envelope);
624 } catch (Exception e) {
625 // TODO handle all exceptions
626 e.printStackTrace();
627 return null;
628 }
629 }
630
631 private void handleEndSession(String source) {
632 signalProtocolStore.deleteAllSessions(source);
633 }
634
635 public interface ReceiveMessageHandler {
636 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent);
637 }
638
639 private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) {
640 if (message.getGroupInfo().isPresent()) {
641 SignalServiceGroup groupInfo = message.getGroupInfo().get();
642 switch (groupInfo.getType()) {
643 case UPDATE:
644 GroupInfo group;
645 group = groupStore.getGroup(groupInfo.getGroupId());
646 if (group == null) {
647 group = new GroupInfo(groupInfo.getGroupId());
648 }
649
650 if (groupInfo.getAvatar().isPresent()) {
651 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
652 if (avatar.isPointer()) {
653 long avatarId = avatar.asPointer().getId();
654 try {
655 retrieveAttachment(avatar.asPointer());
656 // TODO store group avatar in /avatar/groups folder
657 group.avatarId = avatarId;
658 } catch (IOException | InvalidMessageException e) {
659 System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
660 }
661 }
662 }
663
664 if (groupInfo.getName().isPresent()) {
665 group.name = groupInfo.getName().get();
666 }
667
668 if (groupInfo.getMembers().isPresent()) {
669 group.members.addAll(groupInfo.getMembers().get());
670 }
671
672 groupStore.updateGroup(group);
673 break;
674 case DELIVER:
675 break;
676 case QUIT:
677 group = groupStore.getGroup(groupInfo.getGroupId());
678 if (group != null) {
679 group.members.remove(source);
680 groupStore.updateGroup(group);
681 }
682 break;
683 }
684 }
685 if (message.isEndSession()) {
686 handleEndSession(isSync ? destination : source);
687 }
688 if (message.getAttachments().isPresent()) {
689 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
690 if (attachment.isPointer()) {
691 try {
692 retrieveAttachment(attachment.asPointer());
693 } catch (IOException | InvalidMessageException e) {
694 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
695 }
696 }
697 }
698 }
699 }
700
701 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
702 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
703 SignalServiceMessagePipe messagePipe = null;
704
705 try {
706 messagePipe = messageReceiver.createMessagePipe();
707
708 while (true) {
709 SignalServiceEnvelope envelope;
710 SignalServiceContent content = null;
711 try {
712 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
713 if (!envelope.isReceipt()) {
714 content = decryptMessage(envelope);
715 if (content != null) {
716 if (content.getDataMessage().isPresent()) {
717 SignalServiceDataMessage message = content.getDataMessage().get();
718 handleSignalServiceDataMessage(message, false, envelope.getSource(), username);
719 }
720 if (content.getSyncMessage().isPresent()) {
721 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
722 if (syncMessage.getSent().isPresent()) {
723 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
724 handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get());
725 }
726 if (syncMessage.getRequest().isPresent()) {
727 RequestMessage rm = syncMessage.getRequest().get();
728 if (rm.isContactsRequest()) {
729 try {
730 sendContacts();
731 } catch (EncapsulatedExceptions encapsulatedExceptions) {
732 encapsulatedExceptions.printStackTrace();
733 } catch (UntrustedIdentityException e) {
734 e.printStackTrace();
735 }
736 }
737 if (rm.isGroupsRequest()) {
738 try {
739 sendGroups();
740 } catch (EncapsulatedExceptions encapsulatedExceptions) {
741 encapsulatedExceptions.printStackTrace();
742 } catch (UntrustedIdentityException e) {
743 e.printStackTrace();
744 }
745 }
746 }
747 if (syncMessage.getGroups().isPresent()) {
748 try {
749 DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer()));
750 DeviceGroup g;
751 while ((g = s.read()) != null) {
752 GroupInfo syncGroup = groupStore.getGroup(g.getId());
753 if (syncGroup == null) {
754 syncGroup = new GroupInfo(g.getId());
755 }
756 if (g.getName().isPresent()) {
757 syncGroup.name = g.getName().get();
758 }
759 syncGroup.members.addAll(g.getMembers());
760 syncGroup.active = g.isActive();
761
762 if (g.getAvatar().isPresent()) {
763 byte[] ava = new byte[(int) g.getAvatar().get().getLength()];
764 org.whispersystems.signalservice.internal.util.Util.readFully(g.getAvatar().get().getInputStream(), ava);
765 // TODO store group avatar in /avatar/groups folder
766 }
767 groupStore.updateGroup(syncGroup);
768 }
769 } catch (Exception e) {
770 e.printStackTrace();
771 }
772 }
773 if (syncMessage.getContacts().isPresent()) {
774 try {
775 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer()));
776 DeviceContact c;
777 while ((c = s.read()) != null) {
778 ContactInfo contact = new ContactInfo();
779 contact.number = c.getNumber();
780 if (c.getName().isPresent()) {
781 contact.name = c.getName().get();
782 }
783 contactStore.updateContact(contact);
784
785 if (c.getAvatar().isPresent()) {
786 byte[] ava = new byte[(int) c.getAvatar().get().getLength()];
787 org.whispersystems.signalservice.internal.util.Util.readFully(c.getAvatar().get().getInputStream(), ava);
788 // TODO store contact avatar in /avatar/contacts folder
789 }
790 }
791 } catch (Exception e) {
792 e.printStackTrace();
793 }
794 }
795 }
796 }
797 }
798 save();
799 handler.handleMessage(envelope, content);
800 } catch (TimeoutException e) {
801 if (returnOnTimeout)
802 return;
803 } catch (InvalidVersionException e) {
804 System.err.println("Ignoring error: " + e.getMessage());
805 }
806 }
807 } finally {
808 if (messagePipe != null)
809 messagePipe.shutdown();
810 }
811 }
812
813 public File getAttachmentFile(long attachmentId) {
814 return new File(attachmentsPath, attachmentId + "");
815 }
816
817 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
818 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
819
820 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
821 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
822
823 new File(attachmentsPath).mkdirs();
824 File outputFile = getAttachmentFile(pointer.getId());
825 OutputStream output = null;
826 try {
827 output = new FileOutputStream(outputFile);
828 byte[] buffer = new byte[4096];
829 int read;
830
831 while ((read = input.read(buffer)) != -1) {
832 output.write(buffer, 0, read);
833 }
834 } catch (FileNotFoundException e) {
835 e.printStackTrace();
836 return null;
837 } finally {
838 if (output != null) {
839 output.close();
840 output = null;
841 }
842 if (!tmpFile.delete()) {
843 System.err.println("Failed to delete temp file: " + tmpFile);
844 }
845 }
846 if (pointer.getPreview().isPresent()) {
847 File previewFile = new File(outputFile + ".preview");
848 try {
849 output = new FileOutputStream(previewFile);
850 byte[] preview = pointer.getPreview().get();
851 output.write(preview, 0, preview.length);
852 } catch (FileNotFoundException e) {
853 e.printStackTrace();
854 return null;
855 } finally {
856 if (output != null) {
857 output.close();
858 }
859 }
860 }
861 return outputFile;
862 }
863
864 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
865 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
866 File file = File.createTempFile("ts_tmp", "tmp");
867 file.deleteOnExit();
868
869 return messageReceiver.retrieveAttachment(pointer, file);
870 }
871
872 private String canonicalizeNumber(String number) throws InvalidNumberException {
873 String localNumber = username;
874 return PhoneNumberFormatter.formatNumber(number, localNumber);
875 }
876
877 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
878 String e164number = canonicalizeNumber(number);
879 return new SignalServiceAddress(e164number);
880 }
881
882 @Override
883 public boolean isRemote() {
884 return false;
885 }
886
887 private void sendGroups() throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
888 File groupsFile = File.createTempFile("multidevice-group-update", ".tmp");
889
890 try {
891 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile));
892 try {
893 for (GroupInfo record : groupStore.getGroups()) {
894 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
895 new ArrayList<>(record.members), Optional.<SignalServiceAttachmentStream>absent(), // TODO add avatar
896 record.active));
897 }
898 } finally {
899 out.close();
900 }
901
902 if (groupsFile.exists() && groupsFile.length() > 0) {
903 FileInputStream contactsFileStream = new FileInputStream(groupsFile);
904 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
905 .withStream(contactsFileStream)
906 .withContentType("application/octet-stream")
907 .withLength(groupsFile.length())
908 .build();
909
910 sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
911 }
912 } finally {
913 groupsFile.delete();
914 }
915 }
916
917 private void sendContacts() throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
918 File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp");
919
920 try {
921 DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile));
922 try {
923 for (ContactInfo record : contactStore.getContacts()) {
924 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
925 Optional.<SignalServiceAttachmentStream>absent())); // TODO add avatar
926 }
927 } finally {
928 out.close();
929 }
930
931 if (contactsFile.exists() && contactsFile.length() > 0) {
932 FileInputStream contactsFileStream = new FileInputStream(contactsFile);
933 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
934 .withStream(contactsFileStream)
935 .withContentType("application/octet-stream")
936 .withLength(contactsFile.length())
937 .build();
938
939 sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
940 }
941 } finally {
942 contactsFile.delete();
943 }
944 }
945
946 public ContactInfo getContact(String number) {
947 return contactStore.getContact(number);
948 }
949
950 public GroupInfo getGroup(byte[] groupId) {
951 return groupStore.getGroup(groupId);
952 }
953 }