]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
Add command to update account attributes
[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, 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, 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 @Override
585 public void sendGroupMessage(String messageText, List<String> attachments,
586 byte[] groupId)
587 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
588 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
589 if (attachments != null) {
590 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
591 }
592 if (groupId != null) {
593 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
594 .withId(groupId)
595 .build();
596 messageBuilder.asGroupMessage(group);
597 }
598 ThreadInfo thread = threadStore.getThread(Base64.encodeBytes(groupId));
599 if (thread != null) {
600 messageBuilder.withExpiration(thread.messageExpirationTime);
601 }
602
603 final GroupInfo g = getGroupForSending(groupId);
604
605 // Don't send group message to ourself
606 final List<String> membersSend = new ArrayList<>(g.members);
607 membersSend.remove(this.username);
608 sendMessage(messageBuilder, membersSend);
609 }
610
611 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
612 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
613 .withId(groupId)
614 .build();
615
616 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
617 .asGroupMessage(group);
618
619 final GroupInfo g = getGroupForSending(groupId);
620 g.members.remove(this.username);
621 groupStore.updateGroup(g);
622
623 sendMessage(messageBuilder, g.members);
624 }
625
626 private static String join(CharSequence separator, Iterable<? extends CharSequence> list) {
627 StringBuilder buf = new StringBuilder();
628 for (CharSequence str : list) {
629 if (buf.length() > 0) {
630 buf.append(separator);
631 }
632 buf.append(str);
633 }
634
635 return buf.toString();
636 }
637
638 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
639 GroupInfo g;
640 if (groupId == null) {
641 // Create new group
642 g = new GroupInfo(Util.getSecretBytes(16));
643 g.members.add(username);
644 } else {
645 g = getGroupForSending(groupId);
646 }
647
648 if (name != null) {
649 g.name = name;
650 }
651
652 if (members != null) {
653 Set<String> newMembers = new HashSet<>();
654 for (String member : members) {
655 try {
656 member = canonicalizeNumber(member);
657 } catch (InvalidNumberException e) {
658 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
659 System.err.println("Aborting…");
660 System.exit(1);
661 }
662 if (g.members.contains(member)) {
663 continue;
664 }
665 newMembers.add(member);
666 g.members.add(member);
667 }
668 final List<ContactTokenDetails> contacts = accountManager.getContacts(newMembers);
669 if (contacts.size() != newMembers.size()) {
670 // Some of the new members are not registered on Signal
671 for (ContactTokenDetails contact : contacts) {
672 newMembers.remove(contact.getNumber());
673 }
674 System.err.println("Failed to add members " + join(", ", newMembers) + " to group: Not registered on Signal");
675 System.err.println("Aborting…");
676 System.exit(1);
677 }
678 }
679
680 if (avatarFile != null) {
681 createPrivateDirectories(avatarsPath);
682 File aFile = getGroupAvatarFile(g.groupId);
683 Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
684 }
685
686 groupStore.updateGroup(g);
687
688 SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
689
690 // Don't send group message to ourself
691 final List<String> membersSend = new ArrayList<>(g.members);
692 membersSend.remove(this.username);
693 sendMessage(messageBuilder, membersSend);
694 return g.groupId;
695 }
696
697 private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
698 if (groupId == null) {
699 return;
700 }
701 GroupInfo g = getGroupForSending(groupId);
702
703 if (!g.members.contains(recipient)) {
704 return;
705 }
706
707 SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
708
709 // Send group message only to the recipient who requested it
710 final List<String> membersSend = new ArrayList<>();
711 membersSend.add(recipient);
712 sendMessage(messageBuilder, membersSend);
713 }
714
715 private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) {
716 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
717 .withId(g.groupId)
718 .withName(g.name)
719 .withMembers(new ArrayList<>(g.members));
720
721 File aFile = getGroupAvatarFile(g.groupId);
722 if (aFile.exists()) {
723 try {
724 group.withAvatar(createAttachment(aFile));
725 } catch (IOException e) {
726 throw new AttachmentInvalidException(aFile.toString(), e);
727 }
728 }
729
730 return SignalServiceDataMessage.newBuilder()
731 .asGroupMessage(group.build());
732 }
733
734 private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
735 if (groupId == null) {
736 return;
737 }
738
739 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO)
740 .withId(groupId);
741
742 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
743 .asGroupMessage(group.build());
744
745 // Send group info request message to the recipient who sent us a message with this groupId
746 final List<String> membersSend = new ArrayList<>();
747 membersSend.add(recipient);
748 sendMessage(messageBuilder, membersSend);
749 }
750
751 @Override
752 public void sendMessage(String message, List<String> attachments, String recipient)
753 throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
754 List<String> recipients = new ArrayList<>(1);
755 recipients.add(recipient);
756 sendMessage(message, attachments, recipients);
757 }
758
759 @Override
760 public void sendMessage(String messageText, List<String> attachments,
761 List<String> recipients)
762 throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
763 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
764 if (attachments != null) {
765 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
766 }
767 sendMessage(messageBuilder, recipients);
768 }
769
770 @Override
771 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
772 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
773 .asEndSessionMessage();
774
775 sendMessage(messageBuilder, recipients);
776 }
777
778 private void requestSyncGroups() throws IOException {
779 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
780 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
781 try {
782 sendSyncMessage(message);
783 } catch (UntrustedIdentityException e) {
784 e.printStackTrace();
785 }
786 }
787
788 private void requestSyncContacts() throws IOException {
789 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
790 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
791 try {
792 sendSyncMessage(message);
793 } catch (UntrustedIdentityException e) {
794 e.printStackTrace();
795 }
796 }
797
798 private void sendSyncMessage(SignalServiceSyncMessage message)
799 throws IOException, UntrustedIdentityException {
800 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password,
801 deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
802 try {
803 messageSender.sendMessage(message);
804 } catch (UntrustedIdentityException e) {
805 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
806 throw e;
807 }
808 }
809
810 private void sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
811 throws EncapsulatedExceptions, IOException {
812 Set<SignalServiceAddress> recipientsTS = getSignalServiceAddresses(recipients);
813 if (recipientsTS == null) return;
814
815 SignalServiceDataMessage message = null;
816 try {
817 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password,
818 deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
819
820 message = messageBuilder.build();
821 if (message.getGroupInfo().isPresent()) {
822 try {
823 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
824 } catch (EncapsulatedExceptions encapsulatedExceptions) {
825 for (UntrustedIdentityException e : encapsulatedExceptions.getUntrustedIdentityExceptions()) {
826 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
827 }
828 }
829 } else {
830 // Send to all individually, so sync messages are sent correctly
831 List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
832 List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
833 List<NetworkFailureException> networkExceptions = new LinkedList<>();
834 for (SignalServiceAddress address : recipientsTS) {
835 ThreadInfo thread = threadStore.getThread(address.getNumber());
836 if (thread != null) {
837 messageBuilder.withExpiration(thread.messageExpirationTime);
838 } else {
839 messageBuilder.withExpiration(0);
840 }
841 message = messageBuilder.build();
842 try {
843 messageSender.sendMessage(address, message);
844 } catch (UntrustedIdentityException e) {
845 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
846 untrustedIdentities.add(e);
847 } catch (UnregisteredUserException e) {
848 unregisteredUsers.add(e);
849 } catch (PushNetworkException e) {
850 networkExceptions.add(new NetworkFailureException(address.getNumber(), e));
851 }
852 }
853 if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
854 throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
855 }
856 }
857 } finally {
858 if (message != null && message.isEndSession()) {
859 for (SignalServiceAddress recipient : recipientsTS) {
860 handleEndSession(recipient.getNumber());
861 }
862 }
863 save();
864 }
865 }
866
867 private Set<SignalServiceAddress> getSignalServiceAddresses(Collection<String> recipients) {
868 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
869 for (String recipient : recipients) {
870 try {
871 recipientsTS.add(getPushAddress(recipient));
872 } catch (InvalidNumberException e) {
873 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
874 System.err.println("Aborting sending.");
875 save();
876 return null;
877 }
878 }
879 return recipientsTS;
880 }
881
882 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException {
883 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
884 try {
885 return cipher.decrypt(envelope);
886 } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
887 signalProtocolStore.saveIdentity(e.getName(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
888 throw e;
889 }
890 }
891
892 private void handleEndSession(String source) {
893 signalProtocolStore.deleteAllSessions(source);
894 }
895
896 public interface ReceiveMessageHandler {
897 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e);
898 }
899
900 private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination, boolean ignoreAttachments) {
901 String threadId;
902 if (message.getGroupInfo().isPresent()) {
903 SignalServiceGroup groupInfo = message.getGroupInfo().get();
904 threadId = Base64.encodeBytes(groupInfo.getGroupId());
905 GroupInfo group = groupStore.getGroup(groupInfo.getGroupId());
906 switch (groupInfo.getType()) {
907 case UPDATE:
908 if (group == null) {
909 group = new GroupInfo(groupInfo.getGroupId());
910 }
911
912 if (groupInfo.getAvatar().isPresent()) {
913 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
914 if (avatar.isPointer()) {
915 try {
916 retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
917 } catch (IOException | InvalidMessageException e) {
918 System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage());
919 }
920 }
921 }
922
923 if (groupInfo.getName().isPresent()) {
924 group.name = groupInfo.getName().get();
925 }
926
927 if (groupInfo.getMembers().isPresent()) {
928 group.members.addAll(groupInfo.getMembers().get());
929 }
930
931 groupStore.updateGroup(group);
932 break;
933 case DELIVER:
934 if (group == null) {
935 try {
936 sendGroupInfoRequest(groupInfo.getGroupId(), source);
937 } catch (IOException | EncapsulatedExceptions e) {
938 e.printStackTrace();
939 }
940 }
941 break;
942 case QUIT:
943 if (group == null) {
944 try {
945 sendGroupInfoRequest(groupInfo.getGroupId(), source);
946 } catch (IOException | EncapsulatedExceptions e) {
947 e.printStackTrace();
948 }
949 } else {
950 group.members.remove(source);
951 groupStore.updateGroup(group);
952 }
953 break;
954 case REQUEST_INFO:
955 if (group != null) {
956 try {
957 sendUpdateGroupMessage(groupInfo.getGroupId(), source);
958 } catch (IOException | EncapsulatedExceptions e) {
959 e.printStackTrace();
960 } catch (NotAGroupMemberException e) {
961 // We have left this group, so don't send a group update message
962 }
963 }
964 break;
965 }
966 } else {
967 if (isSync) {
968 threadId = destination;
969 } else {
970 threadId = source;
971 }
972 }
973 if (message.isEndSession()) {
974 handleEndSession(isSync ? destination : source);
975 }
976 if (message.isExpirationUpdate() || message.getBody().isPresent()) {
977 ThreadInfo thread = threadStore.getThread(threadId);
978 if (thread == null) {
979 thread = new ThreadInfo();
980 thread.id = threadId;
981 }
982 if (thread.messageExpirationTime != message.getExpiresInSeconds()) {
983 thread.messageExpirationTime = message.getExpiresInSeconds();
984 threadStore.updateThread(thread);
985 }
986 }
987 if (message.getAttachments().isPresent() && !ignoreAttachments) {
988 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
989 if (attachment.isPointer()) {
990 try {
991 retrieveAttachment(attachment.asPointer());
992 } catch (IOException | InvalidMessageException e) {
993 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
994 }
995 }
996 }
997 }
998 }
999
1000 public void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
1001 final File cachePath = new File(getMessageCachePath());
1002 if (!cachePath.exists()) {
1003 return;
1004 }
1005 for (final File dir : cachePath.listFiles()) {
1006 if (!dir.isDirectory()) {
1007 continue;
1008 }
1009
1010 for (final File fileEntry : dir.listFiles()) {
1011 if (!fileEntry.isFile()) {
1012 continue;
1013 }
1014 SignalServiceEnvelope envelope;
1015 try {
1016 envelope = loadEnvelope(fileEntry);
1017 if (envelope == null) {
1018 continue;
1019 }
1020 } catch (IOException e) {
1021 e.printStackTrace();
1022 continue;
1023 }
1024 SignalServiceContent content = null;
1025 if (!envelope.isReceipt()) {
1026 try {
1027 content = decryptMessage(envelope);
1028 } catch (Exception e) {
1029 continue;
1030 }
1031 handleMessage(envelope, content, ignoreAttachments);
1032 }
1033 save();
1034 handler.handleMessage(envelope, content, null);
1035 try {
1036 Files.delete(fileEntry.toPath());
1037 } catch (IOException e) {
1038 System.out.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
1039 }
1040 }
1041 }
1042 }
1043
1044 public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException {
1045 retryFailedReceivedMessages(handler, ignoreAttachments);
1046 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1047
1048 try {
1049 if (messagePipe == null) {
1050 messagePipe = messageReceiver.createMessagePipe();
1051 }
1052
1053 while (true) {
1054 SignalServiceEnvelope envelope;
1055 SignalServiceContent content = null;
1056 Exception exception = null;
1057 final long now = new Date().getTime();
1058 try {
1059 envelope = messagePipe.read(timeout, unit, new SignalServiceMessagePipe.MessagePipeCallback() {
1060 @Override
1061 public void onMessage(SignalServiceEnvelope envelope) {
1062 // store message on disk, before acknowledging receipt to the server
1063 try {
1064 File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1065 storeEnvelope(envelope, cacheFile);
1066 } catch (IOException e) {
1067 System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
1068 }
1069 }
1070 });
1071 } catch (TimeoutException e) {
1072 if (returnOnTimeout)
1073 return;
1074 continue;
1075 } catch (InvalidVersionException e) {
1076 System.err.println("Ignoring error: " + e.getMessage());
1077 continue;
1078 }
1079 if (!envelope.isReceipt()) {
1080 try {
1081 content = decryptMessage(envelope);
1082 } catch (Exception e) {
1083 exception = e;
1084 }
1085 handleMessage(envelope, content, ignoreAttachments);
1086 }
1087 save();
1088 handler.handleMessage(envelope, content, exception);
1089 if (exception == null || !(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
1090 File cacheFile = null;
1091 try {
1092 cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1093 Files.delete(cacheFile.toPath());
1094 } catch (IOException e) {
1095 System.out.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
1096 }
1097 }
1098 }
1099 } finally {
1100 if (messagePipe != null) {
1101 messagePipe.shutdown();
1102 messagePipe = null;
1103 }
1104 }
1105 }
1106
1107 private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
1108 if (content != null) {
1109 if (content.getDataMessage().isPresent()) {
1110 SignalServiceDataMessage message = content.getDataMessage().get();
1111 handleSignalServiceDataMessage(message, false, envelope.getSource(), username, ignoreAttachments);
1112 }
1113 if (content.getSyncMessage().isPresent()) {
1114 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
1115 if (syncMessage.getSent().isPresent()) {
1116 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
1117 handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get(), ignoreAttachments);
1118 }
1119 if (syncMessage.getRequest().isPresent()) {
1120 RequestMessage rm = syncMessage.getRequest().get();
1121 if (rm.isContactsRequest()) {
1122 try {
1123 sendContacts();
1124 } catch (UntrustedIdentityException | IOException e) {
1125 e.printStackTrace();
1126 }
1127 }
1128 if (rm.isGroupsRequest()) {
1129 try {
1130 sendGroups();
1131 } catch (UntrustedIdentityException | IOException e) {
1132 e.printStackTrace();
1133 }
1134 }
1135 }
1136 if (syncMessage.getGroups().isPresent()) {
1137 File tmpFile = null;
1138 try {
1139 tmpFile = Util.createTempFile();
1140 DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile));
1141 DeviceGroup g;
1142 while ((g = s.read()) != null) {
1143 GroupInfo syncGroup = groupStore.getGroup(g.getId());
1144 if (syncGroup == null) {
1145 syncGroup = new GroupInfo(g.getId());
1146 }
1147 if (g.getName().isPresent()) {
1148 syncGroup.name = g.getName().get();
1149 }
1150 syncGroup.members.addAll(g.getMembers());
1151 syncGroup.active = g.isActive();
1152
1153 if (g.getAvatar().isPresent()) {
1154 retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
1155 }
1156 groupStore.updateGroup(syncGroup);
1157 }
1158 } catch (Exception e) {
1159 e.printStackTrace();
1160 } finally {
1161 if (tmpFile != null) {
1162 try {
1163 Files.delete(tmpFile.toPath());
1164 } catch (IOException e) {
1165 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1166 }
1167 }
1168 }
1169 if (syncMessage.getBlockedList().isPresent()) {
1170 // TODO store list of blocked numbers
1171 }
1172 }
1173 if (syncMessage.getContacts().isPresent()) {
1174 File tmpFile = null;
1175 try {
1176 tmpFile = Util.createTempFile();
1177 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer(), tmpFile));
1178 DeviceContact c;
1179 while ((c = s.read()) != null) {
1180 ContactInfo contact = contactStore.getContact(c.getNumber());
1181 if (contact == null) {
1182 contact = new ContactInfo();
1183 contact.number = c.getNumber();
1184 }
1185 if (c.getName().isPresent()) {
1186 contact.name = c.getName().get();
1187 }
1188 if (c.getColor().isPresent()) {
1189 contact.color = c.getColor().get();
1190 }
1191 contactStore.updateContact(contact);
1192
1193 if (c.getAvatar().isPresent()) {
1194 retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
1195 }
1196 }
1197 } catch (Exception e) {
1198 e.printStackTrace();
1199 } finally {
1200 if (tmpFile != null) {
1201 try {
1202 Files.delete(tmpFile.toPath());
1203 } catch (IOException e) {
1204 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1205 }
1206 }
1207 }
1208 }
1209 }
1210 }
1211 }
1212
1213 private SignalServiceEnvelope loadEnvelope(File file) throws IOException {
1214 try (FileInputStream f = new FileInputStream(file)) {
1215 DataInputStream in = new DataInputStream(f);
1216 int version = in.readInt();
1217 if (version != 1) {
1218 return null;
1219 }
1220 int type = in.readInt();
1221 String source = in.readUTF();
1222 int sourceDevice = in.readInt();
1223 String relay = in.readUTF();
1224 long timestamp = in.readLong();
1225 byte[] content = null;
1226 int contentLen = in.readInt();
1227 if (contentLen > 0) {
1228 content = new byte[contentLen];
1229 in.readFully(content);
1230 }
1231 byte[] legacyMessage = null;
1232 int legacyMessageLen = in.readInt();
1233 if (legacyMessageLen > 0) {
1234 legacyMessage = new byte[legacyMessageLen];
1235 in.readFully(legacyMessage);
1236 }
1237 return new SignalServiceEnvelope(type, source, sourceDevice, relay, timestamp, legacyMessage, content);
1238 }
1239 }
1240
1241 private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
1242 try (FileOutputStream f = new FileOutputStream(file)) {
1243 try (DataOutputStream out = new DataOutputStream(f)) {
1244 out.writeInt(1); // version
1245 out.writeInt(envelope.getType());
1246 out.writeUTF(envelope.getSource());
1247 out.writeInt(envelope.getSourceDevice());
1248 out.writeUTF(envelope.getRelay());
1249 out.writeLong(envelope.getTimestamp());
1250 if (envelope.hasContent()) {
1251 out.writeInt(envelope.getContent().length);
1252 out.write(envelope.getContent());
1253 } else {
1254 out.writeInt(0);
1255 }
1256 if (envelope.hasLegacyMessage()) {
1257 out.writeInt(envelope.getLegacyMessage().length);
1258 out.write(envelope.getLegacyMessage());
1259 } else {
1260 out.writeInt(0);
1261 }
1262 }
1263 }
1264 }
1265
1266 public File getContactAvatarFile(String number) {
1267 return new File(avatarsPath, "contact-" + number);
1268 }
1269
1270 private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
1271 createPrivateDirectories(avatarsPath);
1272 if (attachment.isPointer()) {
1273 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1274 return retrieveAttachment(pointer, getContactAvatarFile(number), false);
1275 } else {
1276 SignalServiceAttachmentStream stream = attachment.asStream();
1277 return retrieveAttachment(stream, getContactAvatarFile(number));
1278 }
1279 }
1280
1281 public File getGroupAvatarFile(byte[] groupId) {
1282 return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
1283 }
1284
1285 private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
1286 createPrivateDirectories(avatarsPath);
1287 if (attachment.isPointer()) {
1288 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1289 return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
1290 } else {
1291 SignalServiceAttachmentStream stream = attachment.asStream();
1292 return retrieveAttachment(stream, getGroupAvatarFile(groupId));
1293 }
1294 }
1295
1296 public File getAttachmentFile(long attachmentId) {
1297 return new File(attachmentsPath, attachmentId + "");
1298 }
1299
1300 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1301 createPrivateDirectories(attachmentsPath);
1302 return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
1303 }
1304
1305 private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
1306 InputStream input = stream.getInputStream();
1307
1308 try (OutputStream output = new FileOutputStream(outputFile)) {
1309 byte[] buffer = new byte[4096];
1310 int read;
1311
1312 while ((read = input.read(buffer)) != -1) {
1313 output.write(buffer, 0, read);
1314 }
1315 } catch (FileNotFoundException e) {
1316 e.printStackTrace();
1317 return null;
1318 }
1319 return outputFile;
1320 }
1321
1322 private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
1323 if (storePreview && pointer.getPreview().isPresent()) {
1324 File previewFile = new File(outputFile + ".preview");
1325 try (OutputStream output = new FileOutputStream(previewFile)) {
1326 byte[] preview = pointer.getPreview().get();
1327 output.write(preview, 0, preview.length);
1328 } catch (FileNotFoundException e) {
1329 e.printStackTrace();
1330 return null;
1331 }
1332 }
1333
1334 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1335
1336 File tmpFile = Util.createTempFile();
1337 try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile)) {
1338 try (OutputStream output = new FileOutputStream(outputFile)) {
1339 byte[] buffer = new byte[4096];
1340 int read;
1341
1342 while ((read = input.read(buffer)) != -1) {
1343 output.write(buffer, 0, read);
1344 }
1345 } catch (FileNotFoundException e) {
1346 e.printStackTrace();
1347 return null;
1348 }
1349 } finally {
1350 try {
1351 Files.delete(tmpFile.toPath());
1352 } catch (IOException e) {
1353 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1354 }
1355 }
1356 return outputFile;
1357 }
1358
1359 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException {
1360 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1361 return messageReceiver.retrieveAttachment(pointer, tmpFile);
1362 }
1363
1364 private String canonicalizeNumber(String number) throws InvalidNumberException {
1365 String localNumber = username;
1366 return PhoneNumberFormatter.formatNumber(number, localNumber);
1367 }
1368
1369 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
1370 String e164number = canonicalizeNumber(number);
1371 return new SignalServiceAddress(e164number);
1372 }
1373
1374 @Override
1375 public boolean isRemote() {
1376 return false;
1377 }
1378
1379 private void sendGroups() throws IOException, UntrustedIdentityException {
1380 File groupsFile = Util.createTempFile();
1381
1382 try {
1383 try (OutputStream fos = new FileOutputStream(groupsFile)) {
1384 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
1385 for (GroupInfo record : groupStore.getGroups()) {
1386 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
1387 new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
1388 record.active));
1389 }
1390 }
1391
1392 if (groupsFile.exists() && groupsFile.length() > 0) {
1393 try (FileInputStream groupsFileStream = new FileInputStream(groupsFile)) {
1394 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1395 .withStream(groupsFileStream)
1396 .withContentType("application/octet-stream")
1397 .withLength(groupsFile.length())
1398 .build();
1399
1400 sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
1401 }
1402 }
1403 } finally {
1404 try {
1405 Files.delete(groupsFile.toPath());
1406 } catch (IOException e) {
1407 System.out.println("Failed to delete temp file “" + groupsFile + "”: " + e.getMessage());
1408 }
1409 }
1410 }
1411
1412 private void sendContacts() throws IOException, UntrustedIdentityException {
1413 File contactsFile = Util.createTempFile();
1414
1415 try {
1416 try (OutputStream fos = new FileOutputStream(contactsFile)) {
1417 DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos);
1418 for (ContactInfo record : contactStore.getContacts()) {
1419 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
1420 createContactAvatarAttachment(record.number), Optional.fromNullable(record.color)));
1421 }
1422 }
1423
1424 if (contactsFile.exists() && contactsFile.length() > 0) {
1425 try (FileInputStream contactsFileStream = new FileInputStream(contactsFile)) {
1426 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1427 .withStream(contactsFileStream)
1428 .withContentType("application/octet-stream")
1429 .withLength(contactsFile.length())
1430 .build();
1431
1432 sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
1433 }
1434 }
1435 } finally {
1436 try {
1437 Files.delete(contactsFile.toPath());
1438 } catch (IOException e) {
1439 System.out.println("Failed to delete temp file “" + contactsFile + "”: " + e.getMessage());
1440 }
1441 }
1442 }
1443
1444 public ContactInfo getContact(String number) {
1445 return contactStore.getContact(number);
1446 }
1447
1448 public GroupInfo getGroup(byte[] groupId) {
1449 return groupStore.getGroup(groupId);
1450 }
1451
1452 public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
1453 return signalProtocolStore.getIdentities();
1454 }
1455
1456 public List<JsonIdentityKeyStore.Identity> getIdentities(String number) {
1457 return signalProtocolStore.getIdentities(number);
1458 }
1459
1460 /**
1461 * Trust this the identity with this fingerprint
1462 *
1463 * @param name username of the identity
1464 * @param fingerprint Fingerprint
1465 */
1466 public boolean trustIdentityVerified(String name, byte[] fingerprint) {
1467 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1468 if (ids == null) {
1469 return false;
1470 }
1471 for (JsonIdentityKeyStore.Identity id : ids) {
1472 if (!Arrays.equals(id.identityKey.serialize(), fingerprint)) {
1473 continue;
1474 }
1475
1476 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED);
1477 save();
1478 return true;
1479 }
1480 return false;
1481 }
1482
1483 /**
1484 * Trust this the identity with this safety number
1485 *
1486 * @param name username of the identity
1487 * @param safetyNumber Safety number
1488 */
1489 public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) {
1490 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1491 if (ids == null) {
1492 return false;
1493 }
1494 for (JsonIdentityKeyStore.Identity id : ids) {
1495 if (!safetyNumber.equals(computeSafetyNumber(name, id.identityKey))) {
1496 continue;
1497 }
1498
1499 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED);
1500 save();
1501 return true;
1502 }
1503 return false;
1504 }
1505
1506 /**
1507 * Trust all keys of this identity without verification
1508 *
1509 * @param name username of the identity
1510 */
1511 public boolean trustIdentityAllKeys(String name) {
1512 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1513 if (ids == null) {
1514 return false;
1515 }
1516 for (JsonIdentityKeyStore.Identity id : ids) {
1517 if (id.trustLevel == TrustLevel.UNTRUSTED) {
1518 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_UNVERIFIED);
1519 }
1520 }
1521 save();
1522 return true;
1523 }
1524
1525 public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) {
1526 Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(username, getIdentity(), theirUsername, theirIdentityKey);
1527 return fingerprint.getDisplayableFingerprint().getDisplayText();
1528 }
1529 }