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