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