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