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