]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
Store encrypted messages on disk when receiving them
[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 receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
868 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
869 SignalServiceMessagePipe messagePipe = null;
870
871 try {
872 messagePipe = messageReceiver.createMessagePipe();
873
874 while (true) {
875 SignalServiceEnvelope envelope;
876 SignalServiceContent content = null;
877 Exception exception = null;
878 final long now = new Date().getTime();
879 try {
880 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS, new SignalServiceMessagePipe.MessagePipeCallback() {
881 @Override
882 public void onMessage(SignalServiceEnvelope envelope) {
883 // store message on disk, before acknowledging receipt to the server
884 try {
885 File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
886 storeEnvelope(envelope, cacheFile);
887 } catch (IOException e) {
888 System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
889 }
890 }
891 });
892 } catch (TimeoutException e) {
893 if (returnOnTimeout)
894 return;
895 continue;
896 } catch (InvalidVersionException e) {
897 System.err.println("Ignoring error: " + e.getMessage());
898 continue;
899 }
900 if (!envelope.isReceipt()) {
901 try {
902 content = decryptMessage(envelope);
903 } catch (Exception e) {
904 exception = e;
905 }
906 handleMessage(envelope, content);
907 }
908 save();
909 handler.handleMessage(envelope, content, exception);
910 if (exception == null || !(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
911 try {
912 File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
913 cacheFile.delete();
914 } catch (IOException e) {
915 // Ignoring
916 return;
917 }
918 }
919 }
920 } finally {
921 if (messagePipe != null)
922 messagePipe.shutdown();
923 }
924 }
925
926 private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content) {
927 if (content != null) {
928 if (content.getDataMessage().isPresent()) {
929 SignalServiceDataMessage message = content.getDataMessage().get();
930 handleSignalServiceDataMessage(message, false, envelope.getSource(), username);
931 }
932 if (content.getSyncMessage().isPresent()) {
933 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
934 if (syncMessage.getSent().isPresent()) {
935 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
936 handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get());
937 }
938 if (syncMessage.getRequest().isPresent()) {
939 RequestMessage rm = syncMessage.getRequest().get();
940 if (rm.isContactsRequest()) {
941 try {
942 sendContacts();
943 } catch (UntrustedIdentityException | IOException e) {
944 e.printStackTrace();
945 }
946 }
947 if (rm.isGroupsRequest()) {
948 try {
949 sendGroups();
950 } catch (UntrustedIdentityException | IOException e) {
951 e.printStackTrace();
952 }
953 }
954 }
955 if (syncMessage.getGroups().isPresent()) {
956 try {
957 DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer()));
958 DeviceGroup g;
959 while ((g = s.read()) != null) {
960 GroupInfo syncGroup = groupStore.getGroup(g.getId());
961 if (syncGroup == null) {
962 syncGroup = new GroupInfo(g.getId());
963 }
964 if (g.getName().isPresent()) {
965 syncGroup.name = g.getName().get();
966 }
967 syncGroup.members.addAll(g.getMembers());
968 syncGroup.active = g.isActive();
969
970 if (g.getAvatar().isPresent()) {
971 retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
972 }
973 groupStore.updateGroup(syncGroup);
974 }
975 } catch (Exception e) {
976 e.printStackTrace();
977 }
978 }
979 if (syncMessage.getContacts().isPresent()) {
980 try {
981 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer()));
982 DeviceContact c;
983 while ((c = s.read()) != null) {
984 ContactInfo contact = new ContactInfo();
985 contact.number = c.getNumber();
986 if (c.getName().isPresent()) {
987 contact.name = c.getName().get();
988 }
989 contactStore.updateContact(contact);
990
991 if (c.getAvatar().isPresent()) {
992 retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
993 }
994 }
995 } catch (Exception e) {
996 e.printStackTrace();
997 }
998 }
999 }
1000 }
1001 }
1002
1003 private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
1004 try (FileOutputStream f = new FileOutputStream(file)) {
1005 DataOutputStream out = new DataOutputStream(f);
1006 out.writeInt(1); // version
1007 out.writeInt(envelope.getType());
1008 out.writeUTF(envelope.getSource());
1009 out.writeInt(envelope.getSourceDevice());
1010 out.writeUTF(envelope.getRelay());
1011 out.writeLong(envelope.getTimestamp());
1012 if (envelope.hasContent()) {
1013 out.writeInt(envelope.getContent().length);
1014 out.write(envelope.getContent());
1015 } else {
1016 out.writeInt(0);
1017 }
1018 if (envelope.hasLegacyMessage()) {
1019 out.writeInt(envelope.getLegacyMessage().length);
1020 out.write(envelope.getLegacyMessage());
1021 } else {
1022 out.writeInt(0);
1023 }
1024 out.close();
1025 }
1026 }
1027
1028 public File getContactAvatarFile(String number) {
1029 return new File(avatarsPath, "contact-" + number);
1030 }
1031
1032 private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
1033 createPrivateDirectories(avatarsPath);
1034 if (attachment.isPointer()) {
1035 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1036 return retrieveAttachment(pointer, getContactAvatarFile(number), false);
1037 } else {
1038 SignalServiceAttachmentStream stream = attachment.asStream();
1039 return retrieveAttachment(stream, getContactAvatarFile(number));
1040 }
1041 }
1042
1043 public File getGroupAvatarFile(byte[] groupId) {
1044 return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
1045 }
1046
1047 private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
1048 createPrivateDirectories(avatarsPath);
1049 if (attachment.isPointer()) {
1050 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1051 return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
1052 } else {
1053 SignalServiceAttachmentStream stream = attachment.asStream();
1054 return retrieveAttachment(stream, getGroupAvatarFile(groupId));
1055 }
1056 }
1057
1058 public File getAttachmentFile(long attachmentId) {
1059 return new File(attachmentsPath, attachmentId + "");
1060 }
1061
1062 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1063 createPrivateDirectories(attachmentsPath);
1064 return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
1065 }
1066
1067 private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
1068 InputStream input = stream.getInputStream();
1069
1070 OutputStream output = null;
1071 try {
1072 output = new FileOutputStream(outputFile);
1073 byte[] buffer = new byte[4096];
1074 int read;
1075
1076 while ((read = input.read(buffer)) != -1) {
1077 output.write(buffer, 0, read);
1078 }
1079 } catch (FileNotFoundException e) {
1080 e.printStackTrace();
1081 return null;
1082 } finally {
1083 if (output != null) {
1084 output.close();
1085 }
1086 }
1087 return outputFile;
1088 }
1089
1090 private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
1091 if (storePreview && pointer.getPreview().isPresent()) {
1092 File previewFile = new File(outputFile + ".preview");
1093 OutputStream output = null;
1094 try {
1095 output = new FileOutputStream(previewFile);
1096 byte[] preview = pointer.getPreview().get();
1097 output.write(preview, 0, preview.length);
1098 } catch (FileNotFoundException e) {
1099 e.printStackTrace();
1100 return null;
1101 } finally {
1102 if (output != null) {
1103 output.close();
1104 }
1105 }
1106 }
1107
1108 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
1109
1110 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
1111 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
1112
1113 OutputStream output = null;
1114 try {
1115 output = new FileOutputStream(outputFile);
1116 byte[] buffer = new byte[4096];
1117 int read;
1118
1119 while ((read = input.read(buffer)) != -1) {
1120 output.write(buffer, 0, read);
1121 }
1122 } catch (FileNotFoundException e) {
1123 e.printStackTrace();
1124 return null;
1125 } finally {
1126 if (output != null) {
1127 output.close();
1128 }
1129 if (!tmpFile.delete()) {
1130 System.err.println("Failed to delete temp file: " + tmpFile);
1131 }
1132 }
1133 return outputFile;
1134 }
1135
1136 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1137 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT);
1138 File file = File.createTempFile("ts_tmp", "tmp");
1139 file.deleteOnExit();
1140
1141 return messageReceiver.retrieveAttachment(pointer, file);
1142 }
1143
1144 private String canonicalizeNumber(String number) throws InvalidNumberException {
1145 String localNumber = username;
1146 return PhoneNumberFormatter.formatNumber(number, localNumber);
1147 }
1148
1149 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
1150 String e164number = canonicalizeNumber(number);
1151 return new SignalServiceAddress(e164number);
1152 }
1153
1154 @Override
1155 public boolean isRemote() {
1156 return false;
1157 }
1158
1159 private void sendGroups() throws IOException, UntrustedIdentityException {
1160 File groupsFile = File.createTempFile("multidevice-group-update", ".tmp");
1161
1162 try {
1163 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(new FileOutputStream(groupsFile));
1164 try {
1165 for (GroupInfo record : groupStore.getGroups()) {
1166 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
1167 new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
1168 record.active));
1169 }
1170 } finally {
1171 out.close();
1172 }
1173
1174 if (groupsFile.exists() && groupsFile.length() > 0) {
1175 FileInputStream contactsFileStream = new FileInputStream(groupsFile);
1176 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1177 .withStream(contactsFileStream)
1178 .withContentType("application/octet-stream")
1179 .withLength(groupsFile.length())
1180 .build();
1181
1182 sendMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
1183 }
1184 } finally {
1185 groupsFile.delete();
1186 }
1187 }
1188
1189 private void sendContacts() throws IOException, UntrustedIdentityException {
1190 File contactsFile = File.createTempFile("multidevice-contact-update", ".tmp");
1191
1192 try {
1193 DeviceContactsOutputStream out = new DeviceContactsOutputStream(new FileOutputStream(contactsFile));
1194 try {
1195 for (ContactInfo record : contactStore.getContacts()) {
1196 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
1197 createContactAvatarAttachment(record.number)));
1198 }
1199 } finally {
1200 out.close();
1201 }
1202
1203 if (contactsFile.exists() && contactsFile.length() > 0) {
1204 FileInputStream contactsFileStream = new FileInputStream(contactsFile);
1205 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1206 .withStream(contactsFileStream)
1207 .withContentType("application/octet-stream")
1208 .withLength(contactsFile.length())
1209 .build();
1210
1211 sendMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
1212 }
1213 } finally {
1214 contactsFile.delete();
1215 }
1216 }
1217
1218 public ContactInfo getContact(String number) {
1219 return contactStore.getContact(number);
1220 }
1221
1222 public GroupInfo getGroup(byte[] groupId) {
1223 return groupStore.getGroup(groupId);
1224 }
1225
1226 public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
1227 return signalProtocolStore.getIdentities();
1228 }
1229
1230 public List<JsonIdentityKeyStore.Identity> getIdentities(String number) {
1231 return signalProtocolStore.getIdentities(number);
1232 }
1233
1234 /**
1235 * Trust this the identity with this fingerprint
1236 *
1237 * @param name username of the identity
1238 * @param fingerprint Fingerprint
1239 */
1240 public boolean trustIdentityVerified(String name, byte[] fingerprint) {
1241 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1242 if (ids == null) {
1243 return false;
1244 }
1245 for (JsonIdentityKeyStore.Identity id : ids) {
1246 if (!Arrays.equals(id.identityKey.serialize(), fingerprint)) {
1247 continue;
1248 }
1249
1250 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_VERIFIED);
1251 save();
1252 return true;
1253 }
1254 return false;
1255 }
1256
1257 /**
1258 * Trust all keys of this identity without verification
1259 *
1260 * @param name username of the identity
1261 */
1262 public boolean trustIdentityAllKeys(String name) {
1263 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1264 if (ids == null) {
1265 return false;
1266 }
1267 for (JsonIdentityKeyStore.Identity id : ids) {
1268 if (id.trustLevel == TrustLevel.UNTRUSTED) {
1269 signalProtocolStore.saveIdentity(name, id.identityKey, TrustLevel.TRUSTED_UNVERIFIED);
1270 }
1271 }
1272 save();
1273 return true;
1274 }
1275 }