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