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