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