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