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