]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
987a5cacd4b147f38bdf474225a66c4496fb196c
[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 private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException {
519 GroupInfo g = groupStore.getGroup(groupId);
520 if (g == null) {
521 throw new GroupNotFoundException(groupId);
522 }
523 for (String member : g.members) {
524 if (member.equals(this.username)) {
525 return g;
526 }
527 }
528 throw new NotAGroupMemberException(groupId, g.name);
529 }
530
531 @Override
532 public void sendGroupMessage(String messageText, List<String> attachments,
533 byte[] groupId)
534 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
535 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
536 if (attachments != null) {
537 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
538 }
539 if (groupId != null) {
540 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
541 .withId(groupId)
542 .build();
543 messageBuilder.asGroupMessage(group);
544 }
545 SignalServiceDataMessage message = messageBuilder.build();
546
547 final GroupInfo g = getGroupForSending(groupId);
548
549 // Don't send group message to ourself
550 final List<String> membersSend = new ArrayList<>(g.members);
551 membersSend.remove(this.username);
552 sendMessage(message, membersSend);
553 }
554
555 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
556 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
557 .withId(groupId)
558 .build();
559
560 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
561 .asGroupMessage(group)
562 .build();
563
564 final GroupInfo g = getGroupForSending(groupId);
565 g.members.remove(this.username);
566 groupStore.updateGroup(g);
567
568 sendMessage(message, g.members);
569 }
570
571 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
572 GroupInfo g;
573 if (groupId == null) {
574 // Create new group
575 g = new GroupInfo(Util.getSecretBytes(16));
576 g.members.add(username);
577 } else {
578 g = getGroupForSending(groupId);
579 }
580
581 if (name != null) {
582 g.name = name;
583 }
584
585 if (members != null) {
586 for (String member : members) {
587 try {
588 g.members.add(canonicalizeNumber(member));
589 } catch (InvalidNumberException e) {
590 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
591 System.err.println("Aborting…");
592 System.exit(1);
593 }
594 }
595 }
596
597 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
598 .withId(g.groupId)
599 .withName(g.name)
600 .withMembers(new ArrayList<>(g.members));
601
602 File aFile = getGroupAvatarFile(g.groupId);
603 if (avatarFile != null) {
604 createPrivateDirectories(avatarsPath);
605 Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
606 }
607 if (aFile.exists()) {
608 try {
609 group.withAvatar(createAttachment(aFile));
610 } catch (IOException e) {
611 throw new AttachmentInvalidException(avatarFile, e);
612 }
613 }
614
615 groupStore.updateGroup(g);
616
617 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
618 .asGroupMessage(group.build())
619 .build();
620
621 // Don't send group message to ourself
622 final List<String> membersSend = new ArrayList<>(g.members);
623 membersSend.remove(this.username);
624 sendMessage(message, membersSend);
625 return g.groupId;
626 }
627
628 @Override
629 public void sendMessage(String message, List<String> attachments, String recipient)
630 throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
631 List<String> recipients = new ArrayList<>(1);
632 recipients.add(recipient);
633 sendMessage(message, attachments, recipients);
634 }
635
636 @Override
637 public void sendMessage(String messageText, List<String> attachments,
638 List<String> recipients)
639 throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
640 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
641 if (attachments != null) {
642 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
643 }
644 SignalServiceDataMessage message = messageBuilder.build();
645
646 sendMessage(message, recipients);
647 }
648
649 @Override
650 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
651 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
652 .asEndSessionMessage()
653 .build();
654
655 sendMessage(message, recipients);
656 }
657
658 private void requestSyncGroups() throws IOException {
659 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
660 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
661 try {
662 sendMessage(message);
663 } catch (UntrustedIdentityException e) {
664 e.printStackTrace();
665 }
666 }
667
668 private void requestSyncContacts() throws IOException {
669 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
670 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
671 try {
672 sendMessage(message);
673 } catch (UntrustedIdentityException e) {
674 e.printStackTrace();
675 }
676 }
677
678 private void sendMessage(SignalServiceSyncMessage message)
679 throws IOException, UntrustedIdentityException {
680 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
681 deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
682 try {
683 messageSender.sendMessage(message);
684 } catch (UntrustedIdentityException e) {
685 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
686 throw e;
687 }
688 }
689
690 private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
691 throws EncapsulatedExceptions, IOException {
692 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
693 for (String recipient : recipients) {
694 try {
695 recipientsTS.add(getPushAddress(recipient));
696 } catch (InvalidNumberException e) {
697 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
698 System.err.println("Aborting sending.");
699 save();
700 return;
701 }
702 }
703
704 try {
705 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
706 deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
707
708 if (message.getGroupInfo().isPresent()) {
709 try {
710 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
711 } catch (EncapsulatedExceptions encapsulatedExceptions) {
712 for (UntrustedIdentityException e : encapsulatedExceptions.getUntrustedIdentityExceptions()) {
713 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
714 }
715 }
716 } else {
717 // Send to all individually, so sync messages are sent correctly
718 List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
719 List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
720 List<NetworkFailureException> networkExceptions = new LinkedList<>();
721 for (SignalServiceAddress address : recipientsTS) {
722 try {
723 messageSender.sendMessage(address, message);
724 } catch (UntrustedIdentityException e) {
725 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
726 untrustedIdentities.add(e);
727 } catch (UnregisteredUserException e) {
728 unregisteredUsers.add(e);
729 } catch (PushNetworkException e) {
730 networkExceptions.add(new NetworkFailureException(address.getNumber(), e));
731 }
732 }
733 if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
734 throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
735 }
736 }
737 } finally {
738 if (message.isEndSession()) {
739 for (SignalServiceAddress recipient : recipientsTS) {
740 handleEndSession(recipient.getNumber());
741 }
742 }
743 save();
744 }
745 }
746
747 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException {
748 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
749 try {
750 return cipher.decrypt(envelope);
751 } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
752 // TODO temporarily store message, until user has accepted the key
753 signalProtocolStore.saveIdentity(e.getName(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
754 throw e;
755 } catch (Exception e) {
756 throw e;
757 }
758 }
759
760 private void handleEndSession(String source) {
761 signalProtocolStore.deleteAllSessions(source);
762 }
763
764 public interface ReceiveMessageHandler {
765 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent);
766 }
767
768 private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) {
769 if (message.getGroupInfo().isPresent()) {
770 SignalServiceGroup groupInfo = message.getGroupInfo().get();
771 switch (groupInfo.getType()) {
772 case UPDATE:
773 GroupInfo group;
774 group = groupStore.getGroup(groupInfo.getGroupId());
775 if (group == null) {
776 group = new GroupInfo(groupInfo.getGroupId());
777 }
778
779 if (groupInfo.getAvatar().isPresent()) {
780 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
781 if (avatar.isPointer()) {
782 try {
783 retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
784 } catch (IOException | InvalidMessageException e) {
785 System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage());
786 }
787 }
788 }
789
790 if (groupInfo.getName().isPresent()) {
791 group.name = groupInfo.getName().get();
792 }
793
794 if (groupInfo.getMembers().isPresent()) {
795 group.members.addAll(groupInfo.getMembers().get());
796 }
797
798 groupStore.updateGroup(group);
799 break;
800 case DELIVER:
801 break;
802 case QUIT:
803 group = groupStore.getGroup(groupInfo.getGroupId());
804 if (group != null) {
805 group.members.remove(source);
806 groupStore.updateGroup(group);
807 }
808 break;
809 }
810 }
811 if (message.isEndSession()) {
812 handleEndSession(isSync ? destination : source);
813 }
814 if (message.getAttachments().isPresent()) {
815 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
816 if (attachment.isPointer()) {
817 try {
818 retrieveAttachment(attachment.asPointer());
819 } catch (IOException | InvalidMessageException e) {
820 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
821 }
822 }
823 }
824 }
825 }
826
827 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
828 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
829 SignalServiceMessagePipe messagePipe = null;
830
831 try {
832 messagePipe = messageReceiver.createMessagePipe();
833
834 while (true) {
835 SignalServiceEnvelope envelope;
836 SignalServiceContent content = null;
837 try {
838 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
839 if (!envelope.isReceipt()) {
840 Exception exception;
841 try {
842 content = decryptMessage(envelope);
843 } catch (Exception e) {
844 exception = e;
845 // TODO pass exception to handler instead
846 e.printStackTrace();
847 }
848 if (content != null) {
849 if (content.getDataMessage().isPresent()) {
850 SignalServiceDataMessage message = content.getDataMessage().get();
851 handleSignalServiceDataMessage(message, false, envelope.getSource(), username);
852 }
853 if (content.getSyncMessage().isPresent()) {
854 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
855 if (syncMessage.getSent().isPresent()) {
856 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
857 handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get());
858 }
859 if (syncMessage.getRequest().isPresent()) {
860 RequestMessage rm = syncMessage.getRequest().get();
861 if (rm.isContactsRequest()) {
862 try {
863 sendContacts();
864 } catch (UntrustedIdentityException e) {
865 e.printStackTrace();
866 }
867 }
868 if (rm.isGroupsRequest()) {
869 try {
870 sendGroups();
871 } catch (UntrustedIdentityException e) {
872 e.printStackTrace();
873 }
874 }
875 }
876 if (syncMessage.getGroups().isPresent()) {
877 try {
878 DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer()));
879 DeviceGroup g;
880 while ((g = s.read()) != null) {
881 GroupInfo syncGroup = groupStore.getGroup(g.getId());
882 if (syncGroup == null) {
883 syncGroup = new GroupInfo(g.getId());
884 }
885 if (g.getName().isPresent()) {
886 syncGroup.name = g.getName().get();
887 }
888 syncGroup.members.addAll(g.getMembers());
889 syncGroup.active = g.isActive();
890
891 if (g.getAvatar().isPresent()) {
892 retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
893 }
894 groupStore.updateGroup(syncGroup);
895 }
896 } catch (Exception e) {
897 e.printStackTrace();
898 }
899 }
900 if (syncMessage.getContacts().isPresent()) {
901 try {
902 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer()));
903 DeviceContact c;
904 while ((c = s.read()) != null) {
905 ContactInfo contact = new ContactInfo();
906 contact.number = c.getNumber();
907 if (c.getName().isPresent()) {
908 contact.name = c.getName().get();
909 }
910 contactStore.updateContact(contact);
911
912 if (c.getAvatar().isPresent()) {
913 retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
914 }
915 }
916 } catch (Exception e) {
917 e.printStackTrace();
918 }
919 }
920 }
921 }
922 }
923 save();
924 handler.handleMessage(envelope, content);
925 } catch (TimeoutException e) {
926 if (returnOnTimeout)
927 return;
928 } catch (InvalidVersionException e) {
929 System.err.println("Ignoring error: " + e.getMessage());
930 }
931 }
932 } finally {
933 if (messagePipe != null)
934 messagePipe.shutdown();
935 }
936 }
937
938 public File getContactAvatarFile(String number) {
939 return new File(avatarsPath, "contact-" + number);
940 }
941
942 private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
943 createPrivateDirectories(avatarsPath);
944 if (attachment.isPointer()) {
945 SignalServiceAttachmentPointer pointer = attachment.asPointer();
946 return retrieveAttachment(pointer, getContactAvatarFile(number), false);
947 } else {
948 SignalServiceAttachmentStream stream = attachment.asStream();
949 return retrieveAttachment(stream, getContactAvatarFile(number));
950 }
951 }
952
953 public File getGroupAvatarFile(byte[] groupId) {
954 return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
955 }
956
957 private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
958 createPrivateDirectories(avatarsPath);
959 if (attachment.isPointer()) {
960 SignalServiceAttachmentPointer pointer = attachment.asPointer();
961 return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
962 } else {
963 SignalServiceAttachmentStream stream = attachment.asStream();
964 return retrieveAttachment(stream, getGroupAvatarFile(groupId));
965 }
966 }
967
968 public File getAttachmentFile(long attachmentId) {
969 return new File(attachmentsPath, attachmentId + "");
970 }
971
972 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
973 createPrivateDirectories(attachmentsPath);
974 return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
975 }
976
977 private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
978 InputStream input = stream.getInputStream();
979
980 OutputStream output = null;
981 try {
982 output = new FileOutputStream(outputFile);
983 byte[] buffer = new byte[4096];
984 int read;
985
986 while ((read = input.read(buffer)) != -1) {
987 output.write(buffer, 0, read);
988 }
989 } catch (FileNotFoundException e) {
990 e.printStackTrace();
991 return null;
992 } finally {
993 if (output != null) {
994 output.close();
995 }
996 }
997 return outputFile;
998 }
999
1000 private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
1001 if (storePreview && pointer.getPreview().isPresent()) {
1002 File previewFile = new File(outputFile + ".preview");
1003 OutputStream output = null;
1004 try {
1005 output = new FileOutputStream(previewFile);
1006 byte[] preview = pointer.getPreview().get();
1007 output.write(preview, 0, preview.length);
1008 } catch (FileNotFoundException e) {
1009 e.printStackTrace();
1010 return null;
1011 } finally {
1012 if (output != null) {
1013 output.close();
1014 }
1015 }
1016 }
1017
1018 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
1019
1020 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
1021 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
1022
1023 OutputStream output = null;
1024 try {
1025 output = new FileOutputStream(outputFile);
1026 byte[] buffer = new byte[4096];
1027 int read;
1028
1029 while ((read = input.read(buffer)) != -1) {
1030 output.write(buffer, 0, read);
1031 }
1032 } catch (FileNotFoundException e) {
1033 e.printStackTrace();
1034 return null;
1035 } finally {
1036 if (output != null) {
1037 output.close();
1038 }
1039 if (!tmpFile.delete()) {
1040 System.err.println("Failed to delete temp file: " + tmpFile);
1041 }
1042 }
1043 return outputFile;
1044 }
1045
1046 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1047 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
1048 File file = File.createTempFile("ts_tmp", "tmp");
1049 file.deleteOnExit();
1050
1051 return messageReceiver.retrieveAttachment(pointer, file);
1052 }
1053
1054 private String canonicalizeNumber(String number) throws InvalidNumberException {
1055 String localNumber = username;
1056 return PhoneNumberFormatter.formatNumber(number, localNumber);
1057 }
1058
1059 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
1060 String e164number = canonicalizeNumber(number);
1061 return new SignalServiceAddress(e164number);
1062 }
1063
1064 @Override
1065 public boolean isRemote() {
1066 return false;
1067 }
1068
1069 private void sendGroups() throws IOException, UntrustedIdentityException {
1070 File groupsFile = File.createTempFile("multidevice-group-update", ".tmp");
1071
1072 try {
1073 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile));
1074 try {
1075 for (GroupInfo record : groupStore.getGroups()) {
1076 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
1077 new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
1078 record.active));
1079 }
1080 } finally {
1081 out.close();
1082 }
1083
1084 if (groupsFile.exists() && groupsFile.length() > 0) {
1085 FileInputStream contactsFileStream = new FileInputStream(groupsFile);
1086 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1087 .withStream(contactsFileStream)
1088 .withContentType("application/octet-stream")
1089 .withLength(groupsFile.length())
1090 .build();
1091
1092 sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
1093 }
1094 } finally {
1095 groupsFile.delete();
1096 }
1097 }
1098
1099 private void sendContacts() throws IOException, UntrustedIdentityException {
1100 File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp");
1101
1102 try {
1103 DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile));
1104 try {
1105 for (ContactInfo record : contactStore.getContacts()) {
1106 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
1107 createContactAvatarAttachment(record.number)));
1108 }
1109 } finally {
1110 out.close();
1111 }
1112
1113 if (contactsFile.exists() && contactsFile.length() > 0) {
1114 FileInputStream contactsFileStream = new FileInputStream(contactsFile);
1115 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1116 .withStream(contactsFileStream)
1117 .withContentType("application/octet-stream")
1118 .withLength(contactsFile.length())
1119 .build();
1120
1121 sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
1122 }
1123 } finally {
1124 contactsFile.delete();
1125 }
1126 }
1127
1128 public ContactInfo getContact(String number) {
1129 return contactStore.getContact(number);
1130 }
1131
1132 public GroupInfo getGroup(byte[] groupId) {
1133 return groupStore.getGroup(groupId);
1134 }
1135
1136 public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
1137 return signalProtocolStore.getIdentities();
1138 }
1139
1140 public List<JsonIdentityKeyStore.Identity> getIdentities(String number) {
1141 return signalProtocolStore.getIdentities(number);
1142 }
1143
1144 /**
1145 * Trust this the identity with this fingerprint
1146 *
1147 * @param name username of the identity
1148 * @param fingerprint Fingerprint
1149 */
1150 public boolean trustIdentityVerified(String name, byte[] fingerprint) {
1151 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1152 if (ids == null) {
1153 return false;
1154 }
1155 for (JsonIdentityKeyStore.Identity id : ids) {
1156 if (!Arrays.equals(id.identityKey.serialize(), fingerprint)) {
1157 continue;
1158 }
1159
1160 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED);
1161 save();
1162 return true;
1163 }
1164 return false;
1165 }
1166
1167 /**
1168 * Trust all keys of this identity without verification
1169 *
1170 * @param name username of the identity
1171 */
1172 public boolean trustIdentityAllKeys(String name) {
1173 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1174 if (ids == null) {
1175 return false;
1176 }
1177 for (JsonIdentityKeyStore.Identity id : ids) {
1178 if (id.trustLevel == TrustLevel.UNTRUSTED) {
1179 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_UNVERIFIED);
1180 }
1181 }
1182 save();
1183 return true;
1184 }
1185 }