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