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