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