]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
Moving files to packages
[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 void updateAccountAttributes() throws IOException {
360 accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false,true);
361 }
362
363 public void unregister() throws IOException {
364 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
365 // If this is the master device, other users can't send messages to this number anymore.
366 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
367 accountManager.setGcmId(Optional.<String>absent());
368 }
369
370 public URI getDeviceLinkUri() throws TimeoutException, IOException {
371 password = Util.getSecret(18);
372
373 accountManager = new SignalServiceAccountManager(serviceUrls, username, password, USER_AGENT);
374 String uuid = accountManager.getNewDeviceUuid();
375
376 registered = false;
377 try {
378 return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8"));
379 } catch (URISyntaxException e) {
380 // Shouldn't happen
381 return null;
382 }
383 }
384
385 public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
386 signalingKey = Util.getSecret(52);
387 SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName);
388 deviceId = ret.getDeviceId();
389 username = ret.getNumber();
390 // TODO do this check before actually registering
391 if (userExists()) {
392 throw new UserAlreadyExists(username, getFileName());
393 }
394 signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId());
395
396 registered = true;
397 refreshPreKeys();
398
399 requestSyncGroups();
400 requestSyncContacts();
401
402 save();
403 }
404
405 public List<DeviceInfo> getLinkedDevices() throws IOException {
406 return accountManager.getDevices();
407 }
408
409 public void removeLinkedDevices(int deviceId) throws IOException {
410 accountManager.removeDevice(deviceId);
411 }
412
413 public static Map<String, String> getQueryMap(String query) {
414 String[] params = query.split("&");
415 Map<String, String> map = new HashMap<>();
416 for (String param : params) {
417 String name = null;
418 try {
419 name = URLDecoder.decode(param.split("=")[0], "utf-8");
420 } catch (UnsupportedEncodingException e) {
421 // Impossible
422 }
423 String value = null;
424 try {
425 value = URLDecoder.decode(param.split("=")[1], "utf-8");
426 } catch (UnsupportedEncodingException e) {
427 // Impossible
428 }
429 map.put(name, value);
430 }
431 return map;
432 }
433
434 public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
435 Map<String, String> query = getQueryMap(linkUri.getRawQuery());
436 String deviceIdentifier = query.get("uuid");
437 String publicKeyEncoded = query.get("pub_key");
438
439 if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) {
440 throw new RuntimeException("Invalid device link uri");
441 }
442
443 ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
444
445 addDevice(deviceIdentifier, deviceKey);
446 }
447
448 private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
449 IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair();
450 String verificationCode = accountManager.getNewDeviceVerificationCode();
451
452 accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, verificationCode);
453 }
454
455 private List<PreKeyRecord> generatePreKeys() {
456 List<PreKeyRecord> records = new LinkedList<>();
457
458 for (int i = 0; i < PREKEY_BATCH_SIZE; i++) {
459 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
460 ECKeyPair keyPair = Curve.generateKeyPair();
461 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
462
463 signalProtocolStore.storePreKey(preKeyId, record);
464 records.add(record);
465 }
466
467 preKeyIdOffset = (preKeyIdOffset + PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE;
468 save();
469
470 return records;
471 }
472
473 private PreKeyRecord getOrGenerateLastResortPreKey() {
474 if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) {
475 try {
476 return signalProtocolStore.loadPreKey(Medium.MAX_VALUE);
477 } catch (InvalidKeyIdException e) {
478 signalProtocolStore.removePreKey(Medium.MAX_VALUE);
479 }
480 }
481
482 ECKeyPair keyPair = Curve.generateKeyPair();
483 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
484
485 signalProtocolStore.storePreKey(Medium.MAX_VALUE, record);
486 save();
487
488 return record;
489 }
490
491 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
492 try {
493 ECKeyPair keyPair = Curve.generateKeyPair();
494 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
495 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
496
497 signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record);
498 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
499 save();
500
501 return record;
502 } catch (InvalidKeyException e) {
503 throw new AssertionError(e);
504 }
505 }
506
507 public void verifyAccount(String verificationCode) throws IOException {
508 verificationCode = verificationCode.replace("-", "");
509 signalingKey = Util.getSecret(52);
510 accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false,true);
511
512 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
513 registered = true;
514
515 refreshPreKeys();
516 save();
517 }
518
519 private void refreshPreKeys() throws IOException {
520 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
521 PreKeyRecord lastResortKey = getOrGenerateLastResortPreKey();
522 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair());
523
524 accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
525 }
526
527
528 private static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
529 List<SignalServiceAttachment> SignalServiceAttachments = null;
530 if (attachments != null) {
531 SignalServiceAttachments = new ArrayList<>(attachments.size());
532 for (String attachment : attachments) {
533 try {
534 SignalServiceAttachments.add(createAttachment(new File(attachment)));
535 } catch (IOException e) {
536 throw new AttachmentInvalidException(attachment, e);
537 }
538 }
539 }
540 return SignalServiceAttachments;
541 }
542
543 private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
544 InputStream attachmentStream = new FileInputStream(attachmentFile);
545 final long attachmentSize = attachmentFile.length();
546 String mime = Files.probeContentType(attachmentFile.toPath());
547 if (mime == null) {
548 mime = "application/octet-stream";
549 }
550 return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null);
551 }
552
553 private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException {
554 File file = getGroupAvatarFile(groupId);
555 if (!file.exists()) {
556 return Optional.absent();
557 }
558
559 return Optional.of(createAttachment(file));
560 }
561
562 private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(String number) throws IOException {
563 File file = getContactAvatarFile(number);
564 if (!file.exists()) {
565 return Optional.absent();
566 }
567
568 return Optional.of(createAttachment(file));
569 }
570
571 private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException {
572 GroupInfo g = groupStore.getGroup(groupId);
573 if (g == null) {
574 throw new GroupNotFoundException(groupId);
575 }
576 for (String member : g.members) {
577 if (member.equals(this.username)) {
578 return g;
579 }
580 }
581 throw new NotAGroupMemberException(groupId, g.name);
582 }
583
584 public List<GroupInfo> getGroups() {
585 return groupStore.getGroups();
586 }
587
588 @Override
589 public void sendGroupMessage(String messageText, List<String> attachments,
590 byte[] groupId)
591 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
592 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
593 if (attachments != null) {
594 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
595 }
596 if (groupId != null) {
597 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
598 .withId(groupId)
599 .build();
600 messageBuilder.asGroupMessage(group);
601 }
602 ThreadInfo thread = threadStore.getThread(Base64.encodeBytes(groupId));
603 if (thread != null) {
604 messageBuilder.withExpiration(thread.messageExpirationTime);
605 }
606
607 final GroupInfo g = getGroupForSending(groupId);
608
609 // Don't send group message to ourself
610 final List<String> membersSend = new ArrayList<>(g.members);
611 membersSend.remove(this.username);
612 sendMessage(messageBuilder, membersSend);
613 }
614
615 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
616 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
617 .withId(groupId)
618 .build();
619
620 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
621 .asGroupMessage(group);
622
623 final GroupInfo g = getGroupForSending(groupId);
624 g.members.remove(this.username);
625 groupStore.updateGroup(g);
626
627 sendMessage(messageBuilder, g.members);
628 }
629
630 private static String join(CharSequence separator, Iterable<? extends CharSequence> list) {
631 StringBuilder buf = new StringBuilder();
632 for (CharSequence str : list) {
633 if (buf.length() > 0) {
634 buf.append(separator);
635 }
636 buf.append(str);
637 }
638
639 return buf.toString();
640 }
641
642 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
643 GroupInfo g;
644 if (groupId == null) {
645 // Create new group
646 g = new GroupInfo(Util.getSecretBytes(16));
647 g.members.add(username);
648 } else {
649 g = getGroupForSending(groupId);
650 }
651
652 if (name != null) {
653 g.name = name;
654 }
655
656 if (members != null) {
657 Set<String> newMembers = new HashSet<>();
658 for (String member : members) {
659 try {
660 member = canonicalizeNumber(member);
661 } catch (InvalidNumberException e) {
662 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
663 System.err.println("Aborting…");
664 System.exit(1);
665 }
666 if (g.members.contains(member)) {
667 continue;
668 }
669 newMembers.add(member);
670 g.members.add(member);
671 }
672 final List<ContactTokenDetails> contacts = accountManager.getContacts(newMembers);
673 if (contacts.size() != newMembers.size()) {
674 // Some of the new members are not registered on Signal
675 for (ContactTokenDetails contact : contacts) {
676 newMembers.remove(contact.getNumber());
677 }
678 System.err.println("Failed to add members " + join(", ", newMembers) + " to group: Not registered on Signal");
679 System.err.println("Aborting…");
680 System.exit(1);
681 }
682 }
683
684 if (avatarFile != null) {
685 createPrivateDirectories(avatarsPath);
686 File aFile = getGroupAvatarFile(g.groupId);
687 Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
688 }
689
690 groupStore.updateGroup(g);
691
692 SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
693
694 // Don't send group message to ourself
695 final List<String> membersSend = new ArrayList<>(g.members);
696 membersSend.remove(this.username);
697 sendMessage(messageBuilder, membersSend);
698 return g.groupId;
699 }
700
701 private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
702 if (groupId == null) {
703 return;
704 }
705 GroupInfo g = getGroupForSending(groupId);
706
707 if (!g.members.contains(recipient)) {
708 return;
709 }
710
711 SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
712
713 // Send group message only to the recipient who requested it
714 final List<String> membersSend = new ArrayList<>();
715 membersSend.add(recipient);
716 sendMessage(messageBuilder, membersSend);
717 }
718
719 private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) {
720 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
721 .withId(g.groupId)
722 .withName(g.name)
723 .withMembers(new ArrayList<>(g.members));
724
725 File aFile = getGroupAvatarFile(g.groupId);
726 if (aFile.exists()) {
727 try {
728 group.withAvatar(createAttachment(aFile));
729 } catch (IOException e) {
730 throw new AttachmentInvalidException(aFile.toString(), e);
731 }
732 }
733
734 return SignalServiceDataMessage.newBuilder()
735 .asGroupMessage(group.build());
736 }
737
738 private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
739 if (groupId == null) {
740 return;
741 }
742
743 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO)
744 .withId(groupId);
745
746 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
747 .asGroupMessage(group.build());
748
749 // Send group info request message to the recipient who sent us a message with this groupId
750 final List<String> membersSend = new ArrayList<>();
751 membersSend.add(recipient);
752 sendMessage(messageBuilder, membersSend);
753 }
754
755 @Override
756 public void sendMessage(String message, List<String> attachments, String recipient)
757 throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
758 List<String> recipients = new ArrayList<>(1);
759 recipients.add(recipient);
760 sendMessage(message, attachments, recipients);
761 }
762
763 @Override
764 public void sendMessage(String messageText, List<String> attachments,
765 List<String> recipients)
766 throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
767 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
768 if (attachments != null) {
769 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
770 }
771 sendMessage(messageBuilder, recipients);
772 }
773
774 @Override
775 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
776 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
777 .asEndSessionMessage();
778
779 sendMessage(messageBuilder, recipients);
780 }
781
782 @Override
783 public String getContactName(String number) {
784 ContactInfo contact = contactStore.getContact(number);
785 if (contact == null) {
786 return "";
787 } else {
788 return contact.name;
789 }
790 }
791
792 @Override
793 public void setContactName(String number, String name) {
794 ContactInfo contact = contactStore.getContact(number);
795 if (contact == null) {
796 contact = new ContactInfo();
797 contact.number = number;
798 System.out.println("Add contact " + number + " named " + name);
799 } else {
800 System.out.println("Updating contact " + number + " name " + contact.name + " -> " + name);
801 }
802 contact.name = name;
803 contactStore.updateContact(contact);
804 save();
805 }
806
807 @Override
808 public String getGroupName(byte[] groupId) {
809 GroupInfo group = getGroup(groupId);
810 if (group == null) {
811 return "";
812 } else {
813 return group.name;
814 }
815 }
816
817 @Override
818 public List<String> getGroupMembers(byte[] groupId) {
819 GroupInfo group = getGroup(groupId);
820 if (group == null) {
821 return new ArrayList<String>();
822 } else {
823 return new ArrayList<String>(group.members);
824 }
825 }
826
827 @Override
828 public void updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
829 if (name.isEmpty()) {
830 name = null;
831 }
832 if (members.size() == 0) {
833 members = null;
834 }
835 if (avatar.isEmpty()) {
836 avatar = null;
837 }
838 sendUpdateGroupMessage(groupId, name, members, avatar);
839 }
840
841 private void requestSyncGroups() throws IOException {
842 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
843 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
844 try {
845 sendSyncMessage(message);
846 } catch (UntrustedIdentityException e) {
847 e.printStackTrace();
848 }
849 }
850
851 private void requestSyncContacts() throws IOException {
852 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
853 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
854 try {
855 sendSyncMessage(message);
856 } catch (UntrustedIdentityException e) {
857 e.printStackTrace();
858 }
859 }
860
861 private void sendSyncMessage(SignalServiceSyncMessage message)
862 throws IOException, UntrustedIdentityException {
863 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password,
864 deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
865 try {
866 messageSender.sendMessage(message);
867 } catch (UntrustedIdentityException e) {
868 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
869 throw e;
870 }
871 }
872
873 private void sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
874 throws EncapsulatedExceptions, IOException {
875 Set<SignalServiceAddress> recipientsTS = getSignalServiceAddresses(recipients);
876 if (recipientsTS == null) return;
877
878 SignalServiceDataMessage message = null;
879 try {
880 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password,
881 deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
882
883 message = messageBuilder.build();
884 if (message.getGroupInfo().isPresent()) {
885 try {
886 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
887 } catch (EncapsulatedExceptions encapsulatedExceptions) {
888 for (UntrustedIdentityException e : encapsulatedExceptions.getUntrustedIdentityExceptions()) {
889 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
890 }
891 }
892 } else {
893 // Send to all individually, so sync messages are sent correctly
894 List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
895 List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
896 List<NetworkFailureException> networkExceptions = new LinkedList<>();
897 for (SignalServiceAddress address : recipientsTS) {
898 ThreadInfo thread = threadStore.getThread(address.getNumber());
899 if (thread != null) {
900 messageBuilder.withExpiration(thread.messageExpirationTime);
901 } else {
902 messageBuilder.withExpiration(0);
903 }
904 message = messageBuilder.build();
905 try {
906 messageSender.sendMessage(address, message);
907 } catch (UntrustedIdentityException e) {
908 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
909 untrustedIdentities.add(e);
910 } catch (UnregisteredUserException e) {
911 unregisteredUsers.add(e);
912 } catch (PushNetworkException e) {
913 networkExceptions.add(new NetworkFailureException(address.getNumber(), e));
914 }
915 }
916 if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
917 throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
918 }
919 }
920 } finally {
921 if (message != null && message.isEndSession()) {
922 for (SignalServiceAddress recipient : recipientsTS) {
923 handleEndSession(recipient.getNumber());
924 }
925 }
926 save();
927 }
928 }
929
930 private Set<SignalServiceAddress> getSignalServiceAddresses(Collection<String> recipients) {
931 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
932 for (String recipient : recipients) {
933 try {
934 recipientsTS.add(getPushAddress(recipient));
935 } catch (InvalidNumberException e) {
936 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
937 System.err.println("Aborting sending.");
938 save();
939 return null;
940 }
941 }
942 return recipientsTS;
943 }
944
945 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException {
946 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
947 try {
948 return cipher.decrypt(envelope);
949 } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
950 signalProtocolStore.saveIdentity(e.getName(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
951 throw e;
952 }
953 }
954
955 private void handleEndSession(String source) {
956 signalProtocolStore.deleteAllSessions(source);
957 }
958
959 public interface ReceiveMessageHandler {
960 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e);
961 }
962
963 private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination, boolean ignoreAttachments) {
964 String threadId;
965 if (message.getGroupInfo().isPresent()) {
966 SignalServiceGroup groupInfo = message.getGroupInfo().get();
967 threadId = Base64.encodeBytes(groupInfo.getGroupId());
968 GroupInfo group = groupStore.getGroup(groupInfo.getGroupId());
969 switch (groupInfo.getType()) {
970 case UPDATE:
971 if (group == null) {
972 group = new GroupInfo(groupInfo.getGroupId());
973 }
974
975 if (groupInfo.getAvatar().isPresent()) {
976 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
977 if (avatar.isPointer()) {
978 try {
979 retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
980 } catch (IOException | InvalidMessageException e) {
981 System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage());
982 }
983 }
984 }
985
986 if (groupInfo.getName().isPresent()) {
987 group.name = groupInfo.getName().get();
988 }
989
990 if (groupInfo.getMembers().isPresent()) {
991 group.members.addAll(groupInfo.getMembers().get());
992 }
993
994 groupStore.updateGroup(group);
995 break;
996 case DELIVER:
997 if (group == null) {
998 try {
999 sendGroupInfoRequest(groupInfo.getGroupId(), source);
1000 } catch (IOException | EncapsulatedExceptions e) {
1001 e.printStackTrace();
1002 }
1003 }
1004 break;
1005 case QUIT:
1006 if (group == null) {
1007 try {
1008 sendGroupInfoRequest(groupInfo.getGroupId(), source);
1009 } catch (IOException | EncapsulatedExceptions e) {
1010 e.printStackTrace();
1011 }
1012 } else {
1013 group.members.remove(source);
1014 groupStore.updateGroup(group);
1015 }
1016 break;
1017 case REQUEST_INFO:
1018 if (group != null) {
1019 try {
1020 sendUpdateGroupMessage(groupInfo.getGroupId(), source);
1021 } catch (IOException | EncapsulatedExceptions e) {
1022 e.printStackTrace();
1023 } catch (NotAGroupMemberException e) {
1024 // We have left this group, so don't send a group update message
1025 }
1026 }
1027 break;
1028 }
1029 } else {
1030 if (isSync) {
1031 threadId = destination;
1032 } else {
1033 threadId = source;
1034 }
1035 }
1036 if (message.isEndSession()) {
1037 handleEndSession(isSync ? destination : source);
1038 }
1039 if (message.isExpirationUpdate() || message.getBody().isPresent()) {
1040 ThreadInfo thread = threadStore.getThread(threadId);
1041 if (thread == null) {
1042 thread = new ThreadInfo();
1043 thread.id = threadId;
1044 }
1045 if (thread.messageExpirationTime != message.getExpiresInSeconds()) {
1046 thread.messageExpirationTime = message.getExpiresInSeconds();
1047 threadStore.updateThread(thread);
1048 }
1049 }
1050 if (message.getAttachments().isPresent() && !ignoreAttachments) {
1051 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
1052 if (attachment.isPointer()) {
1053 try {
1054 retrieveAttachment(attachment.asPointer());
1055 } catch (IOException | InvalidMessageException e) {
1056 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
1057 }
1058 }
1059 }
1060 }
1061 }
1062
1063 public void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
1064 final File cachePath = new File(getMessageCachePath());
1065 if (!cachePath.exists()) {
1066 return;
1067 }
1068 for (final File dir : cachePath.listFiles()) {
1069 if (!dir.isDirectory()) {
1070 continue;
1071 }
1072
1073 for (final File fileEntry : dir.listFiles()) {
1074 if (!fileEntry.isFile()) {
1075 continue;
1076 }
1077 SignalServiceEnvelope envelope;
1078 try {
1079 envelope = loadEnvelope(fileEntry);
1080 if (envelope == null) {
1081 continue;
1082 }
1083 } catch (IOException e) {
1084 e.printStackTrace();
1085 continue;
1086 }
1087 SignalServiceContent content = null;
1088 if (!envelope.isReceipt()) {
1089 try {
1090 content = decryptMessage(envelope);
1091 } catch (Exception e) {
1092 continue;
1093 }
1094 handleMessage(envelope, content, ignoreAttachments);
1095 }
1096 save();
1097 handler.handleMessage(envelope, content, null);
1098 try {
1099 Files.delete(fileEntry.toPath());
1100 } catch (IOException e) {
1101 System.out.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
1102 }
1103 }
1104 }
1105 }
1106
1107 public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException {
1108 retryFailedReceivedMessages(handler, ignoreAttachments);
1109 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1110
1111 try {
1112 if (messagePipe == null) {
1113 messagePipe = messageReceiver.createMessagePipe();
1114 }
1115
1116 while (true) {
1117 SignalServiceEnvelope envelope;
1118 SignalServiceContent content = null;
1119 Exception exception = null;
1120 final long now = new Date().getTime();
1121 try {
1122 envelope = messagePipe.read(timeout, unit, new SignalServiceMessagePipe.MessagePipeCallback() {
1123 @Override
1124 public void onMessage(SignalServiceEnvelope envelope) {
1125 // store message on disk, before acknowledging receipt to the server
1126 try {
1127 File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1128 storeEnvelope(envelope, cacheFile);
1129 } catch (IOException e) {
1130 System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
1131 }
1132 }
1133 });
1134 } catch (TimeoutException e) {
1135 if (returnOnTimeout)
1136 return;
1137 continue;
1138 } catch (InvalidVersionException e) {
1139 System.err.println("Ignoring error: " + e.getMessage());
1140 continue;
1141 }
1142 if (!envelope.isReceipt()) {
1143 try {
1144 content = decryptMessage(envelope);
1145 } catch (Exception e) {
1146 exception = e;
1147 }
1148 handleMessage(envelope, content, ignoreAttachments);
1149 }
1150 save();
1151 handler.handleMessage(envelope, content, exception);
1152 if (exception == null || !(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
1153 File cacheFile = null;
1154 try {
1155 cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1156 Files.delete(cacheFile.toPath());
1157 } catch (IOException e) {
1158 System.out.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
1159 }
1160 }
1161 }
1162 } finally {
1163 if (messagePipe != null) {
1164 messagePipe.shutdown();
1165 messagePipe = null;
1166 }
1167 }
1168 }
1169
1170 private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
1171 if (content != null) {
1172 if (content.getDataMessage().isPresent()) {
1173 SignalServiceDataMessage message = content.getDataMessage().get();
1174 handleSignalServiceDataMessage(message, false, envelope.getSource(), username, ignoreAttachments);
1175 }
1176 if (content.getSyncMessage().isPresent()) {
1177 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
1178 if (syncMessage.getSent().isPresent()) {
1179 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
1180 handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get(), ignoreAttachments);
1181 }
1182 if (syncMessage.getRequest().isPresent()) {
1183 RequestMessage rm = syncMessage.getRequest().get();
1184 if (rm.isContactsRequest()) {
1185 try {
1186 sendContacts();
1187 } catch (UntrustedIdentityException | IOException e) {
1188 e.printStackTrace();
1189 }
1190 }
1191 if (rm.isGroupsRequest()) {
1192 try {
1193 sendGroups();
1194 } catch (UntrustedIdentityException | IOException e) {
1195 e.printStackTrace();
1196 }
1197 }
1198 }
1199 if (syncMessage.getGroups().isPresent()) {
1200 File tmpFile = null;
1201 try {
1202 tmpFile = Util.createTempFile();
1203 DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile));
1204 DeviceGroup g;
1205 while ((g = s.read()) != null) {
1206 GroupInfo syncGroup = groupStore.getGroup(g.getId());
1207 if (syncGroup == null) {
1208 syncGroup = new GroupInfo(g.getId());
1209 }
1210 if (g.getName().isPresent()) {
1211 syncGroup.name = g.getName().get();
1212 }
1213 syncGroup.members.addAll(g.getMembers());
1214 syncGroup.active = g.isActive();
1215
1216 if (g.getAvatar().isPresent()) {
1217 retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
1218 }
1219 groupStore.updateGroup(syncGroup);
1220 }
1221 } catch (Exception e) {
1222 e.printStackTrace();
1223 } finally {
1224 if (tmpFile != null) {
1225 try {
1226 Files.delete(tmpFile.toPath());
1227 } catch (IOException e) {
1228 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1229 }
1230 }
1231 }
1232 if (syncMessage.getBlockedList().isPresent()) {
1233 // TODO store list of blocked numbers
1234 }
1235 }
1236 if (syncMessage.getContacts().isPresent()) {
1237 File tmpFile = null;
1238 try {
1239 tmpFile = Util.createTempFile();
1240 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer(), tmpFile));
1241 DeviceContact c;
1242 while ((c = s.read()) != null) {
1243 ContactInfo contact = contactStore.getContact(c.getNumber());
1244 if (contact == null) {
1245 contact = new ContactInfo();
1246 contact.number = c.getNumber();
1247 }
1248 if (c.getName().isPresent()) {
1249 contact.name = c.getName().get();
1250 }
1251 if (c.getColor().isPresent()) {
1252 contact.color = c.getColor().get();
1253 }
1254 contactStore.updateContact(contact);
1255
1256 if (c.getAvatar().isPresent()) {
1257 retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
1258 }
1259 }
1260 } catch (Exception e) {
1261 e.printStackTrace();
1262 } finally {
1263 if (tmpFile != null) {
1264 try {
1265 Files.delete(tmpFile.toPath());
1266 } catch (IOException e) {
1267 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1268 }
1269 }
1270 }
1271 }
1272 }
1273 }
1274 }
1275
1276 private SignalServiceEnvelope loadEnvelope(File file) throws IOException {
1277 try (FileInputStream f = new FileInputStream(file)) {
1278 DataInputStream in = new DataInputStream(f);
1279 int version = in.readInt();
1280 if (version != 1) {
1281 return null;
1282 }
1283 int type = in.readInt();
1284 String source = in.readUTF();
1285 int sourceDevice = in.readInt();
1286 String relay = in.readUTF();
1287 long timestamp = in.readLong();
1288 byte[] content = null;
1289 int contentLen = in.readInt();
1290 if (contentLen > 0) {
1291 content = new byte[contentLen];
1292 in.readFully(content);
1293 }
1294 byte[] legacyMessage = null;
1295 int legacyMessageLen = in.readInt();
1296 if (legacyMessageLen > 0) {
1297 legacyMessage = new byte[legacyMessageLen];
1298 in.readFully(legacyMessage);
1299 }
1300 return new SignalServiceEnvelope(type, source, sourceDevice, relay, timestamp, legacyMessage, content);
1301 }
1302 }
1303
1304 private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
1305 try (FileOutputStream f = new FileOutputStream(file)) {
1306 try (DataOutputStream out = new DataOutputStream(f)) {
1307 out.writeInt(1); // version
1308 out.writeInt(envelope.getType());
1309 out.writeUTF(envelope.getSource());
1310 out.writeInt(envelope.getSourceDevice());
1311 out.writeUTF(envelope.getRelay());
1312 out.writeLong(envelope.getTimestamp());
1313 if (envelope.hasContent()) {
1314 out.writeInt(envelope.getContent().length);
1315 out.write(envelope.getContent());
1316 } else {
1317 out.writeInt(0);
1318 }
1319 if (envelope.hasLegacyMessage()) {
1320 out.writeInt(envelope.getLegacyMessage().length);
1321 out.write(envelope.getLegacyMessage());
1322 } else {
1323 out.writeInt(0);
1324 }
1325 }
1326 }
1327 }
1328
1329 public File getContactAvatarFile(String number) {
1330 return new File(avatarsPath, "contact-" + number);
1331 }
1332
1333 private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
1334 createPrivateDirectories(avatarsPath);
1335 if (attachment.isPointer()) {
1336 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1337 return retrieveAttachment(pointer, getContactAvatarFile(number), false);
1338 } else {
1339 SignalServiceAttachmentStream stream = attachment.asStream();
1340 return retrieveAttachment(stream, getContactAvatarFile(number));
1341 }
1342 }
1343
1344 public File getGroupAvatarFile(byte[] groupId) {
1345 return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
1346 }
1347
1348 private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
1349 createPrivateDirectories(avatarsPath);
1350 if (attachment.isPointer()) {
1351 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1352 return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
1353 } else {
1354 SignalServiceAttachmentStream stream = attachment.asStream();
1355 return retrieveAttachment(stream, getGroupAvatarFile(groupId));
1356 }
1357 }
1358
1359 public File getAttachmentFile(long attachmentId) {
1360 return new File(attachmentsPath, attachmentId + "");
1361 }
1362
1363 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1364 createPrivateDirectories(attachmentsPath);
1365 return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
1366 }
1367
1368 private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
1369 InputStream input = stream.getInputStream();
1370
1371 try (OutputStream output = new FileOutputStream(outputFile)) {
1372 byte[] buffer = new byte[4096];
1373 int read;
1374
1375 while ((read = input.read(buffer)) != -1) {
1376 output.write(buffer, 0, read);
1377 }
1378 } catch (FileNotFoundException e) {
1379 e.printStackTrace();
1380 return null;
1381 }
1382 return outputFile;
1383 }
1384
1385 private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
1386 if (storePreview && pointer.getPreview().isPresent()) {
1387 File previewFile = new File(outputFile + ".preview");
1388 try (OutputStream output = new FileOutputStream(previewFile)) {
1389 byte[] preview = pointer.getPreview().get();
1390 output.write(preview, 0, preview.length);
1391 } catch (FileNotFoundException e) {
1392 e.printStackTrace();
1393 return null;
1394 }
1395 }
1396
1397 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1398
1399 File tmpFile = Util.createTempFile();
1400 try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile)) {
1401 try (OutputStream output = new FileOutputStream(outputFile)) {
1402 byte[] buffer = new byte[4096];
1403 int read;
1404
1405 while ((read = input.read(buffer)) != -1) {
1406 output.write(buffer, 0, read);
1407 }
1408 } catch (FileNotFoundException e) {
1409 e.printStackTrace();
1410 return null;
1411 }
1412 } finally {
1413 try {
1414 Files.delete(tmpFile.toPath());
1415 } catch (IOException e) {
1416 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1417 }
1418 }
1419 return outputFile;
1420 }
1421
1422 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException {
1423 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1424 return messageReceiver.retrieveAttachment(pointer, tmpFile);
1425 }
1426
1427 private String canonicalizeNumber(String number) throws InvalidNumberException {
1428 String localNumber = username;
1429 return PhoneNumberFormatter.formatNumber(number, localNumber);
1430 }
1431
1432 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
1433 String e164number = canonicalizeNumber(number);
1434 return new SignalServiceAddress(e164number);
1435 }
1436
1437 @Override
1438 public boolean isRemote() {
1439 return false;
1440 }
1441
1442 private void sendGroups() throws IOException, UntrustedIdentityException {
1443 File groupsFile = Util.createTempFile();
1444
1445 try {
1446 try (OutputStream fos = new FileOutputStream(groupsFile)) {
1447 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
1448 for (GroupInfo record : groupStore.getGroups()) {
1449 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
1450 new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
1451 record.active));
1452 }
1453 }
1454
1455 if (groupsFile.exists() && groupsFile.length() > 0) {
1456 try (FileInputStream groupsFileStream = new FileInputStream(groupsFile)) {
1457 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1458 .withStream(groupsFileStream)
1459 .withContentType("application/octet-stream")
1460 .withLength(groupsFile.length())
1461 .build();
1462
1463 sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
1464 }
1465 }
1466 } finally {
1467 try {
1468 Files.delete(groupsFile.toPath());
1469 } catch (IOException e) {
1470 System.out.println("Failed to delete temp file “" + groupsFile + "”: " + e.getMessage());
1471 }
1472 }
1473 }
1474
1475 private void sendContacts() throws IOException, UntrustedIdentityException {
1476 File contactsFile = Util.createTempFile();
1477
1478 try {
1479 try (OutputStream fos = new FileOutputStream(contactsFile)) {
1480 DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos);
1481 for (ContactInfo record : contactStore.getContacts()) {
1482 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
1483 createContactAvatarAttachment(record.number), Optional.fromNullable(record.color)));
1484 }
1485 }
1486
1487 if (contactsFile.exists() && contactsFile.length() > 0) {
1488 try (FileInputStream contactsFileStream = new FileInputStream(contactsFile)) {
1489 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1490 .withStream(contactsFileStream)
1491 .withContentType("application/octet-stream")
1492 .withLength(contactsFile.length())
1493 .build();
1494
1495 sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
1496 }
1497 }
1498 } finally {
1499 try {
1500 Files.delete(contactsFile.toPath());
1501 } catch (IOException e) {
1502 System.out.println("Failed to delete temp file “" + contactsFile + "”: " + e.getMessage());
1503 }
1504 }
1505 }
1506
1507 public ContactInfo getContact(String number) {
1508 return contactStore.getContact(number);
1509 }
1510
1511 public GroupInfo getGroup(byte[] groupId) {
1512 return groupStore.getGroup(groupId);
1513 }
1514
1515 public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
1516 return signalProtocolStore.getIdentities();
1517 }
1518
1519 public List<JsonIdentityKeyStore.Identity> getIdentities(String number) {
1520 return signalProtocolStore.getIdentities(number);
1521 }
1522
1523 /**
1524 * Trust this the identity with this fingerprint
1525 *
1526 * @param name username of the identity
1527 * @param fingerprint Fingerprint
1528 */
1529 public boolean trustIdentityVerified(String name, byte[] fingerprint) {
1530 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1531 if (ids == null) {
1532 return false;
1533 }
1534 for (JsonIdentityKeyStore.Identity id : ids) {
1535 if (!Arrays.equals(id.identityKey.serialize(), fingerprint)) {
1536 continue;
1537 }
1538
1539 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED);
1540 save();
1541 return true;
1542 }
1543 return false;
1544 }
1545
1546 /**
1547 * Trust this the identity with this safety number
1548 *
1549 * @param name username of the identity
1550 * @param safetyNumber Safety number
1551 */
1552 public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) {
1553 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1554 if (ids == null) {
1555 return false;
1556 }
1557 for (JsonIdentityKeyStore.Identity id : ids) {
1558 if (!safetyNumber.equals(computeSafetyNumber(name, id.identityKey))) {
1559 continue;
1560 }
1561
1562 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED);
1563 save();
1564 return true;
1565 }
1566 return false;
1567 }
1568
1569 /**
1570 * Trust all keys of this identity without verification
1571 *
1572 * @param name username of the identity
1573 */
1574 public boolean trustIdentityAllKeys(String name) {
1575 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1576 if (ids == null) {
1577 return false;
1578 }
1579 for (JsonIdentityKeyStore.Identity id : ids) {
1580 if (id.trustLevel == TrustLevel.UNTRUSTED) {
1581 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_UNVERIFIED);
1582 }
1583 }
1584 save();
1585 return true;
1586 }
1587
1588 public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) {
1589 Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(username, getIdentity(), theirUsername, theirIdentityKey);
1590 return fingerprint.getDisplayableFingerprint().getDisplayText();
1591 }
1592 }