]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
6bd8c65716e83bb06c20566bd41bccd593c98973
[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 }
1021 if (syncMessage.getContacts().isPresent()) {
1022 try {
1023 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer()));
1024 DeviceContact c;
1025 while ((c = s.read()) != null) {
1026 ContactInfo contact = new ContactInfo();
1027 contact.number = c.getNumber();
1028 if (c.getName().isPresent()) {
1029 contact.name = c.getName().get();
1030 }
1031 contactStore.updateContact(contact);
1032
1033 if (c.getAvatar().isPresent()) {
1034 retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
1035 }
1036 }
1037 } catch (Exception e) {
1038 e.printStackTrace();
1039 }
1040 }
1041 }
1042 }
1043 }
1044
1045 private SignalServiceEnvelope loadEnvelope(File file) throws IOException {
1046 try (FileInputStream f = new FileInputStream(file)) {
1047 DataInputStream in = new DataInputStream(f);
1048 int version = in.readInt();
1049 if (version != 1) {
1050 return null;
1051 }
1052 int type = in.readInt();
1053 String source = in.readUTF();
1054 int sourceDevice = in.readInt();
1055 String relay = in.readUTF();
1056 long timestamp = in.readLong();
1057 byte[] content = null;
1058 int contentLen = in.readInt();
1059 if (contentLen > 0) {
1060 content = new byte[contentLen];
1061 in.readFully(content);
1062 }
1063 byte[] legacyMessage = null;
1064 int legacyMessageLen = in.readInt();
1065 if (legacyMessageLen > 0) {
1066 legacyMessage = new byte[legacyMessageLen];
1067 in.readFully(legacyMessage);
1068 }
1069 return new SignalServiceEnvelope(type, source, sourceDevice, relay, timestamp, legacyMessage, content);
1070 }
1071 }
1072
1073 private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
1074 try (FileOutputStream f = new FileOutputStream(file)) {
1075 DataOutputStream out = new DataOutputStream(f);
1076 out.writeInt(1); // version
1077 out.writeInt(envelope.getType());
1078 out.writeUTF(envelope.getSource());
1079 out.writeInt(envelope.getSourceDevice());
1080 out.writeUTF(envelope.getRelay());
1081 out.writeLong(envelope.getTimestamp());
1082 if (envelope.hasContent()) {
1083 out.writeInt(envelope.getContent().length);
1084 out.write(envelope.getContent());
1085 } else {
1086 out.writeInt(0);
1087 }
1088 if (envelope.hasLegacyMessage()) {
1089 out.writeInt(envelope.getLegacyMessage().length);
1090 out.write(envelope.getLegacyMessage());
1091 } else {
1092 out.writeInt(0);
1093 }
1094 out.close();
1095 }
1096 }
1097
1098 public File getContactAvatarFile(String number) {
1099 return new File(avatarsPath, "contact-" + number);
1100 }
1101
1102 private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
1103 createPrivateDirectories(avatarsPath);
1104 if (attachment.isPointer()) {
1105 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1106 return retrieveAttachment(pointer, getContactAvatarFile(number), false);
1107 } else {
1108 SignalServiceAttachmentStream stream = attachment.asStream();
1109 return retrieveAttachment(stream, getContactAvatarFile(number));
1110 }
1111 }
1112
1113 public File getGroupAvatarFile(byte[] groupId) {
1114 return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
1115 }
1116
1117 private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
1118 createPrivateDirectories(avatarsPath);
1119 if (attachment.isPointer()) {
1120 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1121 return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
1122 } else {
1123 SignalServiceAttachmentStream stream = attachment.asStream();
1124 return retrieveAttachment(stream, getGroupAvatarFile(groupId));
1125 }
1126 }
1127
1128 public File getAttachmentFile(long attachmentId) {
1129 return new File(attachmentsPath, attachmentId + "");
1130 }
1131
1132 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1133 createPrivateDirectories(attachmentsPath);
1134 return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
1135 }
1136
1137 private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
1138 InputStream input = stream.getInputStream();
1139
1140 OutputStream output = null;
1141 try {
1142 output = new FileOutputStream(outputFile);
1143 byte[] buffer = new byte[4096];
1144 int read;
1145
1146 while ((read = input.read(buffer)) != -1) {
1147 output.write(buffer, 0, read);
1148 }
1149 } catch (FileNotFoundException e) {
1150 e.printStackTrace();
1151 return null;
1152 } finally {
1153 if (output != null) {
1154 output.close();
1155 }
1156 }
1157 return outputFile;
1158 }
1159
1160 private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
1161 if (storePreview && pointer.getPreview().isPresent()) {
1162 File previewFile = new File(outputFile + ".preview");
1163 OutputStream output = null;
1164 try {
1165 output = new FileOutputStream(previewFile);
1166 byte[] preview = pointer.getPreview().get();
1167 output.write(preview, 0, preview.length);
1168 } catch (FileNotFoundException e) {
1169 e.printStackTrace();
1170 return null;
1171 } finally {
1172 if (output != null) {
1173 output.close();
1174 }
1175 }
1176 }
1177
1178 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
1179
1180 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
1181 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
1182
1183 OutputStream output = null;
1184 try {
1185 output = new FileOutputStream(outputFile);
1186 byte[] buffer = new byte[4096];
1187 int read;
1188
1189 while ((read = input.read(buffer)) != -1) {
1190 output.write(buffer, 0, read);
1191 }
1192 } catch (FileNotFoundException e) {
1193 e.printStackTrace();
1194 return null;
1195 } finally {
1196 if (output != null) {
1197 output.close();
1198 }
1199 if (!tmpFile.delete()) {
1200 System.err.println("Failed to delete temp file: " + tmpFile);
1201 }
1202 }
1203 return outputFile;
1204 }
1205
1206 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1207 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
1208 File file = File.createTempFile("ts_tmp", "tmp");
1209 file.deleteOnExit();
1210
1211 return messageReceiver.retrieveAttachment(pointer, file);
1212 }
1213
1214 private String canonicalizeNumber(String number) throws InvalidNumberException {
1215 String localNumber = username;
1216 return PhoneNumberFormatter.formatNumber(number, localNumber);
1217 }
1218
1219 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
1220 String e164number = canonicalizeNumber(number);
1221 return new SignalServiceAddress(e164number);
1222 }
1223
1224 @Override
1225 public boolean isRemote() {
1226 return false;
1227 }
1228
1229 private void sendGroups() throws IOException, UntrustedIdentityException {
1230 File groupsFile = File.createTempFile("multidevice-group-update", ".tmp");
1231
1232 try {
1233 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile));
1234 try {
1235 for (GroupInfo record : groupStore.getGroups()) {
1236 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
1237 new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
1238 record.active));
1239 }
1240 } finally {
1241 out.close();
1242 }
1243
1244 if (groupsFile.exists() && groupsFile.length() > 0) {
1245 FileInputStream contactsFileStream = new FileInputStream(groupsFile);
1246 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1247 .withStream(contactsFileStream)
1248 .withContentType("application/octet-stream")
1249 .withLength(groupsFile.length())
1250 .build();
1251
1252 sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
1253 }
1254 } finally {
1255 groupsFile.delete();
1256 }
1257 }
1258
1259 private void sendContacts() throws IOException, UntrustedIdentityException {
1260 File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp");
1261
1262 try {
1263 DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile));
1264 try {
1265 for (ContactInfo record : contactStore.getContacts()) {
1266 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
1267 createContactAvatarAttachment(record.number)));
1268 }
1269 } finally {
1270 out.close();
1271 }
1272
1273 if (contactsFile.exists() && contactsFile.length() > 0) {
1274 FileInputStream contactsFileStream = new FileInputStream(contactsFile);
1275 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1276 .withStream(contactsFileStream)
1277 .withContentType("application/octet-stream")
1278 .withLength(contactsFile.length())
1279 .build();
1280
1281 sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
1282 }
1283 } finally {
1284 contactsFile.delete();
1285 }
1286 }
1287
1288 public ContactInfo getContact(String number) {
1289 return contactStore.getContact(number);
1290 }
1291
1292 public GroupInfo getGroup(byte[] groupId) {
1293 return groupStore.getGroup(groupId);
1294 }
1295
1296 public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
1297 return signalProtocolStore.getIdentities();
1298 }
1299
1300 public List<JsonIdentityKeyStore.Identity> getIdentities(String number) {
1301 return signalProtocolStore.getIdentities(number);
1302 }
1303
1304 /**
1305 * Trust this the identity with this fingerprint
1306 *
1307 * @param name username of the identity
1308 * @param fingerprint Fingerprint
1309 */
1310 public boolean trustIdentityVerified(String name, byte[] fingerprint) {
1311 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1312 if (ids == null) {
1313 return false;
1314 }
1315 for (JsonIdentityKeyStore.Identity id : ids) {
1316 if (!Arrays.equals(id.identityKey.serialize(), fingerprint)) {
1317 continue;
1318 }
1319
1320 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED);
1321 save();
1322 return true;
1323 }
1324 return false;
1325 }
1326
1327 /**
1328 * Trust all keys of this identity without verification
1329 *
1330 * @param name username of the identity
1331 */
1332 public boolean trustIdentityAllKeys(String name) {
1333 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1334 if (ids == null) {
1335 return false;
1336 }
1337 for (JsonIdentityKeyStore.Identity id : ids) {
1338 if (id.trustLevel == TrustLevel.UNTRUSTED) {
1339 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_UNVERIFIED);
1340 }
1341 }
1342 save();
1343 return true;
1344 }
1345 }