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