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