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