]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
Improve return codes
[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 if (mime == null) {
468 mime = "application/octet-stream";
469 }
470 return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null);
471 }
472
473 private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException {
474 File file = getGroupAvatarFile(groupId);
475 if (!file.exists()) {
476 return Optional.absent();
477 }
478
479 return Optional.of(createAttachment(file));
480 }
481
482 private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(String number) throws IOException {
483 File file = getContactAvatarFile(number);
484 if (!file.exists()) {
485 return Optional.absent();
486 }
487
488 return Optional.of(createAttachment(file));
489 }
490
491 @Override
492 public void sendGroupMessage(String messageText, List<String> attachments,
493 byte[] groupId)
494 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
495 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
496 if (attachments != null) {
497 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
498 }
499 if (groupId != null) {
500 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
501 .withId(groupId)
502 .build();
503 messageBuilder.asGroupMessage(group);
504 }
505 SignalServiceDataMessage message = messageBuilder.build();
506
507 GroupInfo g = groupStore.getGroup(groupId);
508 if (g == null) {
509 throw new GroupNotFoundException(groupId);
510 }
511
512 // Don't send group message to ourself
513 final List<String> membersSend = new ArrayList<>(g.members);
514 membersSend.remove(this.username);
515 sendMessage(message, membersSend);
516 }
517
518 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
519 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
520 .withId(groupId)
521 .build();
522
523 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
524 .asGroupMessage(group)
525 .build();
526
527 final GroupInfo g = groupStore.getGroup(groupId);
528 if (g == null) {
529 throw new GroupNotFoundException(groupId);
530 }
531 g.members.remove(this.username);
532 groupStore.updateGroup(g);
533
534 sendMessage(message, g.members);
535 }
536
537 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
538 GroupInfo g;
539 if (groupId == null) {
540 // Create new group
541 g = new GroupInfo(Util.getSecretBytes(16));
542 g.members.add(username);
543 } else {
544 g = groupStore.getGroup(groupId);
545 if (g == null) {
546 throw new GroupNotFoundException(groupId);
547 }
548 }
549
550 if (name != null) {
551 g.name = name;
552 }
553
554 if (members != null) {
555 for (String member : members) {
556 try {
557 g.members.add(canonicalizeNumber(member));
558 } catch (InvalidNumberException e) {
559 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
560 System.err.println("Aborting…");
561 System.exit(1);
562 }
563 }
564 }
565
566 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
567 .withId(g.groupId)
568 .withName(g.name)
569 .withMembers(new ArrayList<>(g.members));
570
571 File aFile = getGroupAvatarFile(g.groupId);
572 if (avatarFile != null) {
573 new File(avatarsPath).mkdirs();
574 Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
575 }
576 if (aFile.exists()) {
577 try {
578 group.withAvatar(createAttachment(aFile));
579 } catch (IOException e) {
580 throw new AttachmentInvalidException(avatarFile, e);
581 }
582 }
583
584 groupStore.updateGroup(g);
585
586 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
587 .asGroupMessage(group.build())
588 .build();
589
590 // Don't send group message to ourself
591 final List<String> membersSend = new ArrayList<>(g.members);
592 membersSend.remove(this.username);
593 sendMessage(message, membersSend);
594 return g.groupId;
595 }
596
597 @Override
598 public void sendMessage(String message, List<String> attachments, String recipient)
599 throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
600 List<String> recipients = new ArrayList<>(1);
601 recipients.add(recipient);
602 sendMessage(message, attachments, recipients);
603 }
604
605 @Override
606 public void sendMessage(String messageText, List<String> attachments,
607 List<String> recipients)
608 throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
609 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
610 if (attachments != null) {
611 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
612 }
613 SignalServiceDataMessage message = messageBuilder.build();
614
615 sendMessage(message, recipients);
616 }
617
618 @Override
619 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
620 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
621 .asEndSessionMessage()
622 .build();
623
624 sendMessage(message, recipients);
625 }
626
627 private void requestSyncGroups() throws IOException {
628 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
629 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
630 try {
631 sendMessage(message);
632 } catch (UntrustedIdentityException e) {
633 e.printStackTrace();
634 }
635 }
636
637 private void requestSyncContacts() throws IOException {
638 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
639 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
640 try {
641 sendMessage(message);
642 } catch (UntrustedIdentityException e) {
643 e.printStackTrace();
644 }
645 }
646
647 private void sendMessage(SignalServiceSyncMessage message)
648 throws IOException, UntrustedIdentityException {
649 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
650 deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
651 messageSender.sendMessage(message);
652 }
653
654 private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
655 throws EncapsulatedExceptions, IOException {
656 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
657 for (String recipient : recipients) {
658 try {
659 recipientsTS.add(getPushAddress(recipient));
660 } catch (InvalidNumberException e) {
661 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
662 System.err.println("Aborting sending.");
663 save();
664 return;
665 }
666 }
667
668 try {
669 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
670 deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
671
672 if (message.getGroupInfo().isPresent()) {
673 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
674 } else {
675 // Send to all individually, so sync messages are sent correctly
676 List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
677 List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
678 List<NetworkFailureException> networkExceptions = new LinkedList<>();
679 for (SignalServiceAddress address : recipientsTS) {
680 try {
681 messageSender.sendMessage(address, message);
682 } catch (UntrustedIdentityException e) {
683 untrustedIdentities.add(e);
684 } catch (UnregisteredUserException e) {
685 unregisteredUsers.add(e);
686 } catch (PushNetworkException e) {
687 networkExceptions.add(new NetworkFailureException(address.getNumber(), e));
688 }
689 }
690 if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
691 throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
692 }
693 }
694 } finally {
695 if (message.isEndSession()) {
696 for (SignalServiceAddress recipient : recipientsTS) {
697 handleEndSession(recipient.getNumber());
698 }
699 }
700 save();
701 }
702 }
703
704 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException {
705 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
706 try {
707 return cipher.decrypt(envelope);
708 } catch (Exception e) {
709 throw e;
710 }
711 }
712
713 private void handleEndSession(String source) {
714 signalProtocolStore.deleteAllSessions(source);
715 }
716
717 public interface ReceiveMessageHandler {
718 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent);
719 }
720
721 private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) {
722 if (message.getGroupInfo().isPresent()) {
723 SignalServiceGroup groupInfo = message.getGroupInfo().get();
724 switch (groupInfo.getType()) {
725 case UPDATE:
726 GroupInfo group;
727 group = groupStore.getGroup(groupInfo.getGroupId());
728 if (group == null) {
729 group = new GroupInfo(groupInfo.getGroupId());
730 }
731
732 if (groupInfo.getAvatar().isPresent()) {
733 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
734 if (avatar.isPointer()) {
735 try {
736 retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
737 } catch (IOException | InvalidMessageException e) {
738 System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage());
739 }
740 }
741 }
742
743 if (groupInfo.getName().isPresent()) {
744 group.name = groupInfo.getName().get();
745 }
746
747 if (groupInfo.getMembers().isPresent()) {
748 group.members.addAll(groupInfo.getMembers().get());
749 }
750
751 groupStore.updateGroup(group);
752 break;
753 case DELIVER:
754 break;
755 case QUIT:
756 group = groupStore.getGroup(groupInfo.getGroupId());
757 if (group != null) {
758 group.members.remove(source);
759 groupStore.updateGroup(group);
760 }
761 break;
762 }
763 }
764 if (message.isEndSession()) {
765 handleEndSession(isSync ? destination : source);
766 }
767 if (message.getAttachments().isPresent()) {
768 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
769 if (attachment.isPointer()) {
770 try {
771 retrieveAttachment(attachment.asPointer());
772 } catch (IOException | InvalidMessageException e) {
773 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
774 }
775 }
776 }
777 }
778 }
779
780 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
781 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
782 SignalServiceMessagePipe messagePipe = null;
783
784 try {
785 messagePipe = messageReceiver.createMessagePipe();
786
787 while (true) {
788 SignalServiceEnvelope envelope;
789 SignalServiceContent content = null;
790 try {
791 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
792 if (!envelope.isReceipt()) {
793 Exception exception;
794 try {
795 content = decryptMessage(envelope);
796 } catch (Exception e) {
797 exception = e;
798 // TODO pass exception to handler instead
799 e.printStackTrace();
800 }
801 if (content != null) {
802 if (content.getDataMessage().isPresent()) {
803 SignalServiceDataMessage message = content.getDataMessage().get();
804 handleSignalServiceDataMessage(message, false, envelope.getSource(), username);
805 }
806 if (content.getSyncMessage().isPresent()) {
807 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
808 if (syncMessage.getSent().isPresent()) {
809 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
810 handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get());
811 }
812 if (syncMessage.getRequest().isPresent()) {
813 RequestMessage rm = syncMessage.getRequest().get();
814 if (rm.isContactsRequest()) {
815 try {
816 sendContacts();
817 } catch (UntrustedIdentityException e) {
818 e.printStackTrace();
819 }
820 }
821 if (rm.isGroupsRequest()) {
822 try {
823 sendGroups();
824 } catch (UntrustedIdentityException e) {
825 e.printStackTrace();
826 }
827 }
828 }
829 if (syncMessage.getGroups().isPresent()) {
830 try {
831 DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer()));
832 DeviceGroup g;
833 while ((g = s.read()) != null) {
834 GroupInfo syncGroup = groupStore.getGroup(g.getId());
835 if (syncGroup == null) {
836 syncGroup = new GroupInfo(g.getId());
837 }
838 if (g.getName().isPresent()) {
839 syncGroup.name = g.getName().get();
840 }
841 syncGroup.members.addAll(g.getMembers());
842 syncGroup.active = g.isActive();
843
844 if (g.getAvatar().isPresent()) {
845 retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
846 }
847 groupStore.updateGroup(syncGroup);
848 }
849 } catch (Exception e) {
850 e.printStackTrace();
851 }
852 }
853 if (syncMessage.getContacts().isPresent()) {
854 try {
855 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer()));
856 DeviceContact c;
857 while ((c = s.read()) != null) {
858 ContactInfo contact = new ContactInfo();
859 contact.number = c.getNumber();
860 if (c.getName().isPresent()) {
861 contact.name = c.getName().get();
862 }
863 contactStore.updateContact(contact);
864
865 if (c.getAvatar().isPresent()) {
866 retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
867 }
868 }
869 } catch (Exception e) {
870 e.printStackTrace();
871 }
872 }
873 }
874 }
875 }
876 save();
877 handler.handleMessage(envelope, content);
878 } catch (TimeoutException e) {
879 if (returnOnTimeout)
880 return;
881 } catch (InvalidVersionException e) {
882 System.err.println("Ignoring error: " + e.getMessage());
883 }
884 }
885 } finally {
886 if (messagePipe != null)
887 messagePipe.shutdown();
888 }
889 }
890
891 public File getContactAvatarFile(String number) {
892 return new File(avatarsPath, "contact-" + number);
893 }
894
895 private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
896 new File(avatarsPath).mkdirs();
897 if (attachment.isPointer()) {
898 SignalServiceAttachmentPointer pointer = attachment.asPointer();
899 return retrieveAttachment(pointer, getContactAvatarFile(number), false);
900 } else {
901 SignalServiceAttachmentStream stream = attachment.asStream();
902 return retrieveAttachment(stream, getContactAvatarFile(number));
903 }
904 }
905
906 public File getGroupAvatarFile(byte[] groupId) {
907 return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
908 }
909
910 private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
911 new File(avatarsPath).mkdirs();
912 if (attachment.isPointer()) {
913 SignalServiceAttachmentPointer pointer = attachment.asPointer();
914 return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
915 } else {
916 SignalServiceAttachmentStream stream = attachment.asStream();
917 return retrieveAttachment(stream, getGroupAvatarFile(groupId));
918 }
919 }
920
921 public File getAttachmentFile(long attachmentId) {
922 return new File(attachmentsPath, attachmentId + "");
923 }
924
925 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
926 new File(attachmentsPath).mkdirs();
927 return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
928 }
929
930 private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
931 InputStream input = stream.getInputStream();
932
933 OutputStream output = null;
934 try {
935 output = new FileOutputStream(outputFile);
936 byte[] buffer = new byte[4096];
937 int read;
938
939 while ((read = input.read(buffer)) != -1) {
940 output.write(buffer, 0, read);
941 }
942 } catch (FileNotFoundException e) {
943 e.printStackTrace();
944 return null;
945 } finally {
946 if (output != null) {
947 output.close();
948 }
949 }
950 return outputFile;
951 }
952
953 private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
954 if (storePreview && pointer.getPreview().isPresent()) {
955 File previewFile = new File(outputFile + ".preview");
956 OutputStream output = null;
957 try {
958 output = new FileOutputStream(previewFile);
959 byte[] preview = pointer.getPreview().get();
960 output.write(preview, 0, preview.length);
961 } catch (FileNotFoundException e) {
962 e.printStackTrace();
963 return null;
964 } finally {
965 if (output != null) {
966 output.close();
967 }
968 }
969 }
970
971 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
972
973 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
974 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
975
976 OutputStream output = null;
977 try {
978 output = new FileOutputStream(outputFile);
979 byte[] buffer = new byte[4096];
980 int read;
981
982 while ((read = input.read(buffer)) != -1) {
983 output.write(buffer, 0, read);
984 }
985 } catch (FileNotFoundException e) {
986 e.printStackTrace();
987 return null;
988 } finally {
989 if (output != null) {
990 output.close();
991 }
992 if (!tmpFile.delete()) {
993 System.err.println("Failed to delete temp file: " + tmpFile);
994 }
995 }
996 return outputFile;
997 }
998
999 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1000 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
1001 File file = File.createTempFile("ts_tmp", "tmp");
1002 file.deleteOnExit();
1003
1004 return messageReceiver.retrieveAttachment(pointer, file);
1005 }
1006
1007 private String canonicalizeNumber(String number) throws InvalidNumberException {
1008 String localNumber = username;
1009 return PhoneNumberFormatter.formatNumber(number, localNumber);
1010 }
1011
1012 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
1013 String e164number = canonicalizeNumber(number);
1014 return new SignalServiceAddress(e164number);
1015 }
1016
1017 @Override
1018 public boolean isRemote() {
1019 return false;
1020 }
1021
1022 private void sendGroups() throws IOException, UntrustedIdentityException {
1023 File groupsFile = File.createTempFile("multidevice-group-update", ".tmp");
1024
1025 try {
1026 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile));
1027 try {
1028 for (GroupInfo record : groupStore.getGroups()) {
1029 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
1030 new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
1031 record.active));
1032 }
1033 } finally {
1034 out.close();
1035 }
1036
1037 if (groupsFile.exists() && groupsFile.length() > 0) {
1038 FileInputStream contactsFileStream = new FileInputStream(groupsFile);
1039 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1040 .withStream(contactsFileStream)
1041 .withContentType("application/octet-stream")
1042 .withLength(groupsFile.length())
1043 .build();
1044
1045 sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
1046 }
1047 } finally {
1048 groupsFile.delete();
1049 }
1050 }
1051
1052 private void sendContacts() throws IOException, UntrustedIdentityException {
1053 File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp");
1054
1055 try {
1056 DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile));
1057 try {
1058 for (ContactInfo record : contactStore.getContacts()) {
1059 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
1060 createContactAvatarAttachment(record.number)));
1061 }
1062 } finally {
1063 out.close();
1064 }
1065
1066 if (contactsFile.exists() && contactsFile.length() > 0) {
1067 FileInputStream contactsFileStream = new FileInputStream(contactsFile);
1068 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1069 .withStream(contactsFileStream)
1070 .withContentType("application/octet-stream")
1071 .withLength(contactsFile.length())
1072 .build();
1073
1074 sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
1075 }
1076 } finally {
1077 contactsFile.delete();
1078 }
1079 }
1080
1081 public ContactInfo getContact(String number) {
1082 return contactStore.getContact(number);
1083 }
1084
1085 public GroupInfo getGroup(byte[] groupId) {
1086 return groupStore.getGroup(groupId);
1087 }
1088 }