]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
Add unregister 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 unregister() throws IOException {
360 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
361 // If this is the master device, other users can't send messages to this number anymore.
362 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
363 accountManager.setGcmId(Optional.<String>absent());
364 }
365
366 public URI getDeviceLinkUri() throws TimeoutException, IOException {
367 password = Util.getSecret(18);
368
369 accountManager = new SignalServiceAccountManager(serviceUrls, username, password, USER_AGENT);
370 String uuid = accountManager.getNewDeviceUuid();
371
372 registered = false;
373 try {
374 return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8"));
375 } catch (URISyntaxException e) {
376 // Shouldn't happen
377 return null;
378 }
379 }
380
381 public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
382 signalingKey = Util.getSecret(52);
383 SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName);
384 deviceId = ret.getDeviceId();
385 username = ret.getNumber();
386 // TODO do this check before actually registering
387 if (userExists()) {
388 throw new UserAlreadyExists(username, getFileName());
389 }
390 signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId());
391
392 registered = true;
393 refreshPreKeys();
394
395 requestSyncGroups();
396 requestSyncContacts();
397
398 save();
399 }
400
401 public List<DeviceInfo> getLinkedDevices() throws IOException {
402 return accountManager.getDevices();
403 }
404
405 public void removeLinkedDevices(int deviceId) throws IOException {
406 accountManager.removeDevice(deviceId);
407 }
408
409 public static Map<String, String> getQueryMap(String query) {
410 String[] params = query.split("&");
411 Map<String, String> map = new HashMap<>();
412 for (String param : params) {
413 String name = null;
414 try {
415 name = URLDecoder.decode(param.split("=")[0], "utf-8");
416 } catch (UnsupportedEncodingException e) {
417 // Impossible
418 }
419 String value = null;
420 try {
421 value = URLDecoder.decode(param.split("=")[1], "utf-8");
422 } catch (UnsupportedEncodingException e) {
423 // Impossible
424 }
425 map.put(name, value);
426 }
427 return map;
428 }
429
430 public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
431 Map<String, String> query = getQueryMap(linkUri.getRawQuery());
432 String deviceIdentifier = query.get("uuid");
433 String publicKeyEncoded = query.get("pub_key");
434
435 if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) {
436 throw new RuntimeException("Invalid device link uri");
437 }
438
439 ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
440
441 addDevice(deviceIdentifier, deviceKey);
442 }
443
444 private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
445 IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair();
446 String verificationCode = accountManager.getNewDeviceVerificationCode();
447
448 accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, verificationCode);
449 }
450
451 private List<PreKeyRecord> generatePreKeys() {
452 List<PreKeyRecord> records = new LinkedList<>();
453
454 for (int i = 0; i < PREKEY_BATCH_SIZE; i++) {
455 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
456 ECKeyPair keyPair = Curve.generateKeyPair();
457 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
458
459 signalProtocolStore.storePreKey(preKeyId, record);
460 records.add(record);
461 }
462
463 preKeyIdOffset = (preKeyIdOffset + PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE;
464 save();
465
466 return records;
467 }
468
469 private PreKeyRecord getOrGenerateLastResortPreKey() {
470 if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) {
471 try {
472 return signalProtocolStore.loadPreKey(Medium.MAX_VALUE);
473 } catch (InvalidKeyIdException e) {
474 signalProtocolStore.removePreKey(Medium.MAX_VALUE);
475 }
476 }
477
478 ECKeyPair keyPair = Curve.generateKeyPair();
479 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
480
481 signalProtocolStore.storePreKey(Medium.MAX_VALUE, record);
482 save();
483
484 return record;
485 }
486
487 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
488 try {
489 ECKeyPair keyPair = Curve.generateKeyPair();
490 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
491 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
492
493 signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record);
494 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
495 save();
496
497 return record;
498 } catch (InvalidKeyException e) {
499 throw new AssertionError(e);
500 }
501 }
502
503 public void verifyAccount(String verificationCode) throws IOException {
504 verificationCode = verificationCode.replace("-", "");
505 signalingKey = Util.getSecret(52);
506 accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, true);
507
508 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
509 registered = true;
510
511 refreshPreKeys();
512 save();
513 }
514
515 private void refreshPreKeys() throws IOException {
516 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
517 PreKeyRecord lastResortKey = getOrGenerateLastResortPreKey();
518 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair());
519
520 accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
521 }
522
523
524 private static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
525 List<SignalServiceAttachment> SignalServiceAttachments = null;
526 if (attachments != null) {
527 SignalServiceAttachments = new ArrayList<>(attachments.size());
528 for (String attachment : attachments) {
529 try {
530 SignalServiceAttachments.add(createAttachment(new File(attachment)));
531 } catch (IOException e) {
532 throw new AttachmentInvalidException(attachment, e);
533 }
534 }
535 }
536 return SignalServiceAttachments;
537 }
538
539 private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
540 InputStream attachmentStream = new FileInputStream(attachmentFile);
541 final long attachmentSize = attachmentFile.length();
542 String mime = Files.probeContentType(attachmentFile.toPath());
543 if (mime == null) {
544 mime = "application/octet-stream";
545 }
546 return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null);
547 }
548
549 private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException {
550 File file = getGroupAvatarFile(groupId);
551 if (!file.exists()) {
552 return Optional.absent();
553 }
554
555 return Optional.of(createAttachment(file));
556 }
557
558 private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(String number) throws IOException {
559 File file = getContactAvatarFile(number);
560 if (!file.exists()) {
561 return Optional.absent();
562 }
563
564 return Optional.of(createAttachment(file));
565 }
566
567 private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException {
568 GroupInfo g = groupStore.getGroup(groupId);
569 if (g == null) {
570 throw new GroupNotFoundException(groupId);
571 }
572 for (String member : g.members) {
573 if (member.equals(this.username)) {
574 return g;
575 }
576 }
577 throw new NotAGroupMemberException(groupId, g.name);
578 }
579
580 @Override
581 public void sendGroupMessage(String messageText, List<String> attachments,
582 byte[] groupId)
583 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
584 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
585 if (attachments != null) {
586 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
587 }
588 if (groupId != null) {
589 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
590 .withId(groupId)
591 .build();
592 messageBuilder.asGroupMessage(group);
593 }
594 ThreadInfo thread = threadStore.getThread(Base64.encodeBytes(groupId));
595 if (thread != null) {
596 messageBuilder.withExpiration(thread.messageExpirationTime);
597 }
598
599 final GroupInfo g = getGroupForSending(groupId);
600
601 // Don't send group message to ourself
602 final List<String> membersSend = new ArrayList<>(g.members);
603 membersSend.remove(this.username);
604 sendMessage(messageBuilder, membersSend);
605 }
606
607 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
608 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
609 .withId(groupId)
610 .build();
611
612 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
613 .asGroupMessage(group);
614
615 final GroupInfo g = getGroupForSending(groupId);
616 g.members.remove(this.username);
617 groupStore.updateGroup(g);
618
619 sendMessage(messageBuilder, g.members);
620 }
621
622 private static String join(CharSequence separator, Iterable<? extends CharSequence> list) {
623 StringBuilder buf = new StringBuilder();
624 for (CharSequence str : list) {
625 if (buf.length() > 0) {
626 buf.append(separator);
627 }
628 buf.append(str);
629 }
630
631 return buf.toString();
632 }
633
634 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
635 GroupInfo g;
636 if (groupId == null) {
637 // Create new group
638 g = new GroupInfo(Util.getSecretBytes(16));
639 g.members.add(username);
640 } else {
641 g = getGroupForSending(groupId);
642 }
643
644 if (name != null) {
645 g.name = name;
646 }
647
648 if (members != null) {
649 Set<String> newMembers = new HashSet<>();
650 for (String member : members) {
651 try {
652 member = canonicalizeNumber(member);
653 } catch (InvalidNumberException e) {
654 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
655 System.err.println("Aborting…");
656 System.exit(1);
657 }
658 if (g.members.contains(member)) {
659 continue;
660 }
661 newMembers.add(member);
662 g.members.add(member);
663 }
664 final List<ContactTokenDetails> contacts = accountManager.getContacts(newMembers);
665 if (contacts.size() != newMembers.size()) {
666 // Some of the new members are not registered on Signal
667 for (ContactTokenDetails contact : contacts) {
668 newMembers.remove(contact.getNumber());
669 }
670 System.err.println("Failed to add members " + join(", ", newMembers) + " to group: Not registered on Signal");
671 System.err.println("Aborting…");
672 System.exit(1);
673 }
674 }
675
676 if (avatarFile != null) {
677 createPrivateDirectories(avatarsPath);
678 File aFile = getGroupAvatarFile(g.groupId);
679 Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
680 }
681
682 groupStore.updateGroup(g);
683
684 SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
685
686 // Don't send group message to ourself
687 final List<String> membersSend = new ArrayList<>(g.members);
688 membersSend.remove(this.username);
689 sendMessage(messageBuilder, membersSend);
690 return g.groupId;
691 }
692
693 private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
694 if (groupId == null) {
695 return;
696 }
697 GroupInfo g = getGroupForSending(groupId);
698
699 if (!g.members.contains(recipient)) {
700 return;
701 }
702
703 SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
704
705 // Send group message only to the recipient who requested it
706 final List<String> membersSend = new ArrayList<>();
707 membersSend.add(recipient);
708 sendMessage(messageBuilder, membersSend);
709 }
710
711 private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) {
712 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
713 .withId(g.groupId)
714 .withName(g.name)
715 .withMembers(new ArrayList<>(g.members));
716
717 File aFile = getGroupAvatarFile(g.groupId);
718 if (aFile.exists()) {
719 try {
720 group.withAvatar(createAttachment(aFile));
721 } catch (IOException e) {
722 throw new AttachmentInvalidException(aFile.toString(), e);
723 }
724 }
725
726 return SignalServiceDataMessage.newBuilder()
727 .asGroupMessage(group.build());
728 }
729
730 private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
731 if (groupId == null) {
732 return;
733 }
734
735 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO)
736 .withId(groupId);
737
738 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
739 .asGroupMessage(group.build());
740
741 // Send group info request message to the recipient who sent us a message with this groupId
742 final List<String> membersSend = new ArrayList<>();
743 membersSend.add(recipient);
744 sendMessage(messageBuilder, membersSend);
745 }
746
747 @Override
748 public void sendMessage(String message, List<String> attachments, String recipient)
749 throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
750 List<String> recipients = new ArrayList<>(1);
751 recipients.add(recipient);
752 sendMessage(message, attachments, recipients);
753 }
754
755 @Override
756 public void sendMessage(String messageText, List<String> attachments,
757 List<String> recipients)
758 throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
759 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
760 if (attachments != null) {
761 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
762 }
763 sendMessage(messageBuilder, recipients);
764 }
765
766 @Override
767 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
768 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
769 .asEndSessionMessage();
770
771 sendMessage(messageBuilder, recipients);
772 }
773
774 private void requestSyncGroups() throws IOException {
775 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).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 requestSyncContacts() throws IOException {
785 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
786 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
787 try {
788 sendSyncMessage(message);
789 } catch (UntrustedIdentityException e) {
790 e.printStackTrace();
791 }
792 }
793
794 private void sendSyncMessage(SignalServiceSyncMessage message)
795 throws IOException, UntrustedIdentityException {
796 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password,
797 deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
798 try {
799 messageSender.sendMessage(message);
800 } catch (UntrustedIdentityException e) {
801 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
802 throw e;
803 }
804 }
805
806 private void sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
807 throws EncapsulatedExceptions, IOException {
808 Set<SignalServiceAddress> recipientsTS = getSignalServiceAddresses(recipients);
809 if (recipientsTS == null) return;
810
811 SignalServiceDataMessage message = null;
812 try {
813 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password,
814 deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
815
816 message = messageBuilder.build();
817 if (message.getGroupInfo().isPresent()) {
818 try {
819 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
820 } catch (EncapsulatedExceptions encapsulatedExceptions) {
821 for (UntrustedIdentityException e : encapsulatedExceptions.getUntrustedIdentityExceptions()) {
822 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
823 }
824 }
825 } else {
826 // Send to all individually, so sync messages are sent correctly
827 List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
828 List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
829 List<NetworkFailureException> networkExceptions = new LinkedList<>();
830 for (SignalServiceAddress address : recipientsTS) {
831 ThreadInfo thread = threadStore.getThread(address.getNumber());
832 if (thread != null) {
833 messageBuilder.withExpiration(thread.messageExpirationTime);
834 } else {
835 messageBuilder.withExpiration(0);
836 }
837 message = messageBuilder.build();
838 try {
839 messageSender.sendMessage(address, message);
840 } catch (UntrustedIdentityException e) {
841 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
842 untrustedIdentities.add(e);
843 } catch (UnregisteredUserException e) {
844 unregisteredUsers.add(e);
845 } catch (PushNetworkException e) {
846 networkExceptions.add(new NetworkFailureException(address.getNumber(), e));
847 }
848 }
849 if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
850 throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
851 }
852 }
853 } finally {
854 if (message != null && message.isEndSession()) {
855 for (SignalServiceAddress recipient : recipientsTS) {
856 handleEndSession(recipient.getNumber());
857 }
858 }
859 save();
860 }
861 }
862
863 private Set<SignalServiceAddress> getSignalServiceAddresses(Collection<String> recipients) {
864 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
865 for (String recipient : recipients) {
866 try {
867 recipientsTS.add(getPushAddress(recipient));
868 } catch (InvalidNumberException e) {
869 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
870 System.err.println("Aborting sending.");
871 save();
872 return null;
873 }
874 }
875 return recipientsTS;
876 }
877
878 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException {
879 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
880 try {
881 return cipher.decrypt(envelope);
882 } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
883 signalProtocolStore.saveIdentity(e.getName(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
884 throw e;
885 }
886 }
887
888 private void handleEndSession(String source) {
889 signalProtocolStore.deleteAllSessions(source);
890 }
891
892 public interface ReceiveMessageHandler {
893 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e);
894 }
895
896 private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination, boolean ignoreAttachments) {
897 String threadId;
898 if (message.getGroupInfo().isPresent()) {
899 SignalServiceGroup groupInfo = message.getGroupInfo().get();
900 threadId = Base64.encodeBytes(groupInfo.getGroupId());
901 GroupInfo group = groupStore.getGroup(groupInfo.getGroupId());
902 switch (groupInfo.getType()) {
903 case UPDATE:
904 if (group == null) {
905 group = new GroupInfo(groupInfo.getGroupId());
906 }
907
908 if (groupInfo.getAvatar().isPresent()) {
909 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
910 if (avatar.isPointer()) {
911 try {
912 retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
913 } catch (IOException | InvalidMessageException e) {
914 System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage());
915 }
916 }
917 }
918
919 if (groupInfo.getName().isPresent()) {
920 group.name = groupInfo.getName().get();
921 }
922
923 if (groupInfo.getMembers().isPresent()) {
924 group.members.addAll(groupInfo.getMembers().get());
925 }
926
927 groupStore.updateGroup(group);
928 break;
929 case DELIVER:
930 if (group == null) {
931 try {
932 sendGroupInfoRequest(groupInfo.getGroupId(), source);
933 } catch (IOException | EncapsulatedExceptions e) {
934 e.printStackTrace();
935 }
936 }
937 break;
938 case QUIT:
939 if (group == null) {
940 try {
941 sendGroupInfoRequest(groupInfo.getGroupId(), source);
942 } catch (IOException | EncapsulatedExceptions e) {
943 e.printStackTrace();
944 }
945 } else {
946 group.members.remove(source);
947 groupStore.updateGroup(group);
948 }
949 break;
950 case REQUEST_INFO:
951 if (group != null) {
952 try {
953 sendUpdateGroupMessage(groupInfo.getGroupId(), source);
954 } catch (IOException | EncapsulatedExceptions e) {
955 e.printStackTrace();
956 } catch (NotAGroupMemberException e) {
957 // We have left this group, so don't send a group update message
958 }
959 }
960 break;
961 }
962 } else {
963 if (isSync) {
964 threadId = destination;
965 } else {
966 threadId = source;
967 }
968 }
969 if (message.isEndSession()) {
970 handleEndSession(isSync ? destination : source);
971 }
972 if (message.isExpirationUpdate() || message.getBody().isPresent()) {
973 ThreadInfo thread = threadStore.getThread(threadId);
974 if (thread == null) {
975 thread = new ThreadInfo();
976 thread.id = threadId;
977 }
978 if (thread.messageExpirationTime != message.getExpiresInSeconds()) {
979 thread.messageExpirationTime = message.getExpiresInSeconds();
980 threadStore.updateThread(thread);
981 }
982 }
983 if (message.getAttachments().isPresent() && !ignoreAttachments) {
984 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
985 if (attachment.isPointer()) {
986 try {
987 retrieveAttachment(attachment.asPointer());
988 } catch (IOException | InvalidMessageException e) {
989 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
990 }
991 }
992 }
993 }
994 }
995
996 public void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
997 final File cachePath = new File(getMessageCachePath());
998 if (!cachePath.exists()) {
999 return;
1000 }
1001 for (final File dir : cachePath.listFiles()) {
1002 if (!dir.isDirectory()) {
1003 continue;
1004 }
1005
1006 for (final File fileEntry : dir.listFiles()) {
1007 if (!fileEntry.isFile()) {
1008 continue;
1009 }
1010 SignalServiceEnvelope envelope;
1011 try {
1012 envelope = loadEnvelope(fileEntry);
1013 if (envelope == null) {
1014 continue;
1015 }
1016 } catch (IOException e) {
1017 e.printStackTrace();
1018 continue;
1019 }
1020 SignalServiceContent content = null;
1021 if (!envelope.isReceipt()) {
1022 try {
1023 content = decryptMessage(envelope);
1024 } catch (Exception e) {
1025 continue;
1026 }
1027 handleMessage(envelope, content, ignoreAttachments);
1028 }
1029 save();
1030 handler.handleMessage(envelope, content, null);
1031 try {
1032 Files.delete(fileEntry.toPath());
1033 } catch (IOException e) {
1034 System.out.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
1035 }
1036 }
1037 }
1038 }
1039
1040 public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException {
1041 retryFailedReceivedMessages(handler, ignoreAttachments);
1042 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1043
1044 try {
1045 if (messagePipe == null) {
1046 messagePipe = messageReceiver.createMessagePipe();
1047 }
1048
1049 while (true) {
1050 SignalServiceEnvelope envelope;
1051 SignalServiceContent content = null;
1052 Exception exception = null;
1053 final long now = new Date().getTime();
1054 try {
1055 envelope = messagePipe.read(timeout, unit, new SignalServiceMessagePipe.MessagePipeCallback() {
1056 @Override
1057 public void onMessage(SignalServiceEnvelope envelope) {
1058 // store message on disk, before acknowledging receipt to the server
1059 try {
1060 File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1061 storeEnvelope(envelope, cacheFile);
1062 } catch (IOException e) {
1063 System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
1064 }
1065 }
1066 });
1067 } catch (TimeoutException e) {
1068 if (returnOnTimeout)
1069 return;
1070 continue;
1071 } catch (InvalidVersionException e) {
1072 System.err.println("Ignoring error: " + e.getMessage());
1073 continue;
1074 }
1075 if (!envelope.isReceipt()) {
1076 try {
1077 content = decryptMessage(envelope);
1078 } catch (Exception e) {
1079 exception = e;
1080 }
1081 handleMessage(envelope, content, ignoreAttachments);
1082 }
1083 save();
1084 handler.handleMessage(envelope, content, exception);
1085 if (exception == null || !(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
1086 File cacheFile = null;
1087 try {
1088 cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1089 Files.delete(cacheFile.toPath());
1090 } catch (IOException e) {
1091 System.out.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
1092 }
1093 }
1094 }
1095 } finally {
1096 if (messagePipe != null) {
1097 messagePipe.shutdown();
1098 messagePipe = null;
1099 }
1100 }
1101 }
1102
1103 private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
1104 if (content != null) {
1105 if (content.getDataMessage().isPresent()) {
1106 SignalServiceDataMessage message = content.getDataMessage().get();
1107 handleSignalServiceDataMessage(message, false, envelope.getSource(), username, ignoreAttachments);
1108 }
1109 if (content.getSyncMessage().isPresent()) {
1110 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
1111 if (syncMessage.getSent().isPresent()) {
1112 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
1113 handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get(), ignoreAttachments);
1114 }
1115 if (syncMessage.getRequest().isPresent()) {
1116 RequestMessage rm = syncMessage.getRequest().get();
1117 if (rm.isContactsRequest()) {
1118 try {
1119 sendContacts();
1120 } catch (UntrustedIdentityException | IOException e) {
1121 e.printStackTrace();
1122 }
1123 }
1124 if (rm.isGroupsRequest()) {
1125 try {
1126 sendGroups();
1127 } catch (UntrustedIdentityException | IOException e) {
1128 e.printStackTrace();
1129 }
1130 }
1131 }
1132 if (syncMessage.getGroups().isPresent()) {
1133 File tmpFile = null;
1134 try {
1135 tmpFile = Util.createTempFile();
1136 DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile));
1137 DeviceGroup g;
1138 while ((g = s.read()) != null) {
1139 GroupInfo syncGroup = groupStore.getGroup(g.getId());
1140 if (syncGroup == null) {
1141 syncGroup = new GroupInfo(g.getId());
1142 }
1143 if (g.getName().isPresent()) {
1144 syncGroup.name = g.getName().get();
1145 }
1146 syncGroup.members.addAll(g.getMembers());
1147 syncGroup.active = g.isActive();
1148
1149 if (g.getAvatar().isPresent()) {
1150 retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
1151 }
1152 groupStore.updateGroup(syncGroup);
1153 }
1154 } catch (Exception e) {
1155 e.printStackTrace();
1156 } finally {
1157 if (tmpFile != null) {
1158 try {
1159 Files.delete(tmpFile.toPath());
1160 } catch (IOException e) {
1161 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1162 }
1163 }
1164 }
1165 if (syncMessage.getBlockedList().isPresent()) {
1166 // TODO store list of blocked numbers
1167 }
1168 }
1169 if (syncMessage.getContacts().isPresent()) {
1170 File tmpFile = null;
1171 try {
1172 tmpFile = Util.createTempFile();
1173 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer(), tmpFile));
1174 DeviceContact c;
1175 while ((c = s.read()) != null) {
1176 ContactInfo contact = contactStore.getContact(c.getNumber());
1177 if (contact == null) {
1178 contact = new ContactInfo();
1179 contact.number = c.getNumber();
1180 }
1181 if (c.getName().isPresent()) {
1182 contact.name = c.getName().get();
1183 }
1184 if (c.getColor().isPresent()) {
1185 contact.color = c.getColor().get();
1186 }
1187 contactStore.updateContact(contact);
1188
1189 if (c.getAvatar().isPresent()) {
1190 retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
1191 }
1192 }
1193 } catch (Exception e) {
1194 e.printStackTrace();
1195 } finally {
1196 if (tmpFile != null) {
1197 try {
1198 Files.delete(tmpFile.toPath());
1199 } catch (IOException e) {
1200 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1201 }
1202 }
1203 }
1204 }
1205 }
1206 }
1207 }
1208
1209 private SignalServiceEnvelope loadEnvelope(File file) throws IOException {
1210 try (FileInputStream f = new FileInputStream(file)) {
1211 DataInputStream in = new DataInputStream(f);
1212 int version = in.readInt();
1213 if (version != 1) {
1214 return null;
1215 }
1216 int type = in.readInt();
1217 String source = in.readUTF();
1218 int sourceDevice = in.readInt();
1219 String relay = in.readUTF();
1220 long timestamp = in.readLong();
1221 byte[] content = null;
1222 int contentLen = in.readInt();
1223 if (contentLen > 0) {
1224 content = new byte[contentLen];
1225 in.readFully(content);
1226 }
1227 byte[] legacyMessage = null;
1228 int legacyMessageLen = in.readInt();
1229 if (legacyMessageLen > 0) {
1230 legacyMessage = new byte[legacyMessageLen];
1231 in.readFully(legacyMessage);
1232 }
1233 return new SignalServiceEnvelope(type, source, sourceDevice, relay, timestamp, legacyMessage, content);
1234 }
1235 }
1236
1237 private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
1238 try (FileOutputStream f = new FileOutputStream(file)) {
1239 try (DataOutputStream out = new DataOutputStream(f)) {
1240 out.writeInt(1); // version
1241 out.writeInt(envelope.getType());
1242 out.writeUTF(envelope.getSource());
1243 out.writeInt(envelope.getSourceDevice());
1244 out.writeUTF(envelope.getRelay());
1245 out.writeLong(envelope.getTimestamp());
1246 if (envelope.hasContent()) {
1247 out.writeInt(envelope.getContent().length);
1248 out.write(envelope.getContent());
1249 } else {
1250 out.writeInt(0);
1251 }
1252 if (envelope.hasLegacyMessage()) {
1253 out.writeInt(envelope.getLegacyMessage().length);
1254 out.write(envelope.getLegacyMessage());
1255 } else {
1256 out.writeInt(0);
1257 }
1258 }
1259 }
1260 }
1261
1262 public File getContactAvatarFile(String number) {
1263 return new File(avatarsPath, "contact-" + number);
1264 }
1265
1266 private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
1267 createPrivateDirectories(avatarsPath);
1268 if (attachment.isPointer()) {
1269 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1270 return retrieveAttachment(pointer, getContactAvatarFile(number), false);
1271 } else {
1272 SignalServiceAttachmentStream stream = attachment.asStream();
1273 return retrieveAttachment(stream, getContactAvatarFile(number));
1274 }
1275 }
1276
1277 public File getGroupAvatarFile(byte[] groupId) {
1278 return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
1279 }
1280
1281 private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
1282 createPrivateDirectories(avatarsPath);
1283 if (attachment.isPointer()) {
1284 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1285 return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
1286 } else {
1287 SignalServiceAttachmentStream stream = attachment.asStream();
1288 return retrieveAttachment(stream, getGroupAvatarFile(groupId));
1289 }
1290 }
1291
1292 public File getAttachmentFile(long attachmentId) {
1293 return new File(attachmentsPath, attachmentId + "");
1294 }
1295
1296 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1297 createPrivateDirectories(attachmentsPath);
1298 return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
1299 }
1300
1301 private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
1302 InputStream input = stream.getInputStream();
1303
1304 try (OutputStream output = new FileOutputStream(outputFile)) {
1305 byte[] buffer = new byte[4096];
1306 int read;
1307
1308 while ((read = input.read(buffer)) != -1) {
1309 output.write(buffer, 0, read);
1310 }
1311 } catch (FileNotFoundException e) {
1312 e.printStackTrace();
1313 return null;
1314 }
1315 return outputFile;
1316 }
1317
1318 private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
1319 if (storePreview && pointer.getPreview().isPresent()) {
1320 File previewFile = new File(outputFile + ".preview");
1321 try (OutputStream output = new FileOutputStream(previewFile)) {
1322 byte[] preview = pointer.getPreview().get();
1323 output.write(preview, 0, preview.length);
1324 } catch (FileNotFoundException e) {
1325 e.printStackTrace();
1326 return null;
1327 }
1328 }
1329
1330 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1331
1332 File tmpFile = Util.createTempFile();
1333 try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile)) {
1334 try (OutputStream output = new FileOutputStream(outputFile)) {
1335 byte[] buffer = new byte[4096];
1336 int read;
1337
1338 while ((read = input.read(buffer)) != -1) {
1339 output.write(buffer, 0, read);
1340 }
1341 } catch (FileNotFoundException e) {
1342 e.printStackTrace();
1343 return null;
1344 }
1345 } finally {
1346 try {
1347 Files.delete(tmpFile.toPath());
1348 } catch (IOException e) {
1349 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1350 }
1351 }
1352 return outputFile;
1353 }
1354
1355 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException {
1356 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1357 return messageReceiver.retrieveAttachment(pointer, tmpFile);
1358 }
1359
1360 private String canonicalizeNumber(String number) throws InvalidNumberException {
1361 String localNumber = username;
1362 return PhoneNumberFormatter.formatNumber(number, localNumber);
1363 }
1364
1365 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
1366 String e164number = canonicalizeNumber(number);
1367 return new SignalServiceAddress(e164number);
1368 }
1369
1370 @Override
1371 public boolean isRemote() {
1372 return false;
1373 }
1374
1375 private void sendGroups() throws IOException, UntrustedIdentityException {
1376 File groupsFile = Util.createTempFile();
1377
1378 try {
1379 try (OutputStream fos = new FileOutputStream(groupsFile)) {
1380 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
1381 for (GroupInfo record : groupStore.getGroups()) {
1382 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
1383 new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
1384 record.active));
1385 }
1386 }
1387
1388 if (groupsFile.exists() && groupsFile.length() > 0) {
1389 try (FileInputStream groupsFileStream = new FileInputStream(groupsFile)) {
1390 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1391 .withStream(groupsFileStream)
1392 .withContentType("application/octet-stream")
1393 .withLength(groupsFile.length())
1394 .build();
1395
1396 sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
1397 }
1398 }
1399 } finally {
1400 try {
1401 Files.delete(groupsFile.toPath());
1402 } catch (IOException e) {
1403 System.out.println("Failed to delete temp file “" + groupsFile + "”: " + e.getMessage());
1404 }
1405 }
1406 }
1407
1408 private void sendContacts() throws IOException, UntrustedIdentityException {
1409 File contactsFile = Util.createTempFile();
1410
1411 try {
1412 try (OutputStream fos = new FileOutputStream(contactsFile)) {
1413 DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos);
1414 for (ContactInfo record : contactStore.getContacts()) {
1415 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
1416 createContactAvatarAttachment(record.number), Optional.fromNullable(record.color)));
1417 }
1418 }
1419
1420 if (contactsFile.exists() && contactsFile.length() > 0) {
1421 try (FileInputStream contactsFileStream = new FileInputStream(contactsFile)) {
1422 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1423 .withStream(contactsFileStream)
1424 .withContentType("application/octet-stream")
1425 .withLength(contactsFile.length())
1426 .build();
1427
1428 sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
1429 }
1430 }
1431 } finally {
1432 try {
1433 Files.delete(contactsFile.toPath());
1434 } catch (IOException e) {
1435 System.out.println("Failed to delete temp file “" + contactsFile + "”: " + e.getMessage());
1436 }
1437 }
1438 }
1439
1440 public ContactInfo getContact(String number) {
1441 return contactStore.getContact(number);
1442 }
1443
1444 public GroupInfo getGroup(byte[] groupId) {
1445 return groupStore.getGroup(groupId);
1446 }
1447
1448 public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
1449 return signalProtocolStore.getIdentities();
1450 }
1451
1452 public List<JsonIdentityKeyStore.Identity> getIdentities(String number) {
1453 return signalProtocolStore.getIdentities(number);
1454 }
1455
1456 /**
1457 * Trust this the identity with this fingerprint
1458 *
1459 * @param name username of the identity
1460 * @param fingerprint Fingerprint
1461 */
1462 public boolean trustIdentityVerified(String name, byte[] fingerprint) {
1463 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1464 if (ids == null) {
1465 return false;
1466 }
1467 for (JsonIdentityKeyStore.Identity id : ids) {
1468 if (!Arrays.equals(id.identityKey.serialize(), fingerprint)) {
1469 continue;
1470 }
1471
1472 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED);
1473 save();
1474 return true;
1475 }
1476 return false;
1477 }
1478
1479 /**
1480 * Trust this the identity with this safety number
1481 *
1482 * @param name username of the identity
1483 * @param safetyNumber Safety number
1484 */
1485 public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) {
1486 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1487 if (ids == null) {
1488 return false;
1489 }
1490 for (JsonIdentityKeyStore.Identity id : ids) {
1491 if (!safetyNumber.equals(computeSafetyNumber(name, id.identityKey))) {
1492 continue;
1493 }
1494
1495 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED);
1496 save();
1497 return true;
1498 }
1499 return false;
1500 }
1501
1502 /**
1503 * Trust all keys of this identity without verification
1504 *
1505 * @param name username of the identity
1506 */
1507 public boolean trustIdentityAllKeys(String name) {
1508 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1509 if (ids == null) {
1510 return false;
1511 }
1512 for (JsonIdentityKeyStore.Identity id : ids) {
1513 if (id.trustLevel == TrustLevel.UNTRUSTED) {
1514 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_UNVERIFIED);
1515 }
1516 }
1517 save();
1518 return true;
1519 }
1520
1521 public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) {
1522 Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(username, getIdentity(), theirUsername, theirIdentityKey);
1523 return fingerprint.getDisplayableFingerprint().getDisplayText();
1524 }
1525 }