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