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