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