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