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