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