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