]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
Update signal-service-java dependency
[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.asamk.signal.storage.contacts.ContactInfo;
31 import org.asamk.signal.storage.contacts.JsonContactsStore;
32 import org.asamk.signal.storage.groups.GroupInfo;
33 import org.asamk.signal.storage.groups.JsonGroupStore;
34 import org.asamk.signal.storage.protocol.JsonIdentityKeyStore;
35 import org.asamk.signal.storage.protocol.JsonSignalProtocolStore;
36 import org.asamk.signal.storage.threads.JsonThreadStore;
37 import org.asamk.signal.storage.threads.ThreadInfo;
38 import org.asamk.signal.util.KeyUtils;
39 import org.asamk.signal.util.Util;
40 import org.signal.libsignal.metadata.*;
41 import org.signal.libsignal.metadata.certificate.CertificateValidator;
42 import org.whispersystems.libsignal.*;
43 import org.whispersystems.libsignal.ecc.Curve;
44 import org.whispersystems.libsignal.ecc.ECKeyPair;
45 import org.whispersystems.libsignal.ecc.ECPublicKey;
46 import org.whispersystems.libsignal.fingerprint.Fingerprint;
47 import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
48 import org.whispersystems.libsignal.state.PreKeyRecord;
49 import org.whispersystems.libsignal.state.SignedPreKeyRecord;
50 import org.whispersystems.libsignal.util.KeyHelper;
51 import org.whispersystems.libsignal.util.Medium;
52 import org.whispersystems.libsignal.util.guava.Optional;
53 import org.whispersystems.signalservice.api.SignalServiceAccountManager;
54 import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
55 import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
56 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
57 import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
58 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
59 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
60 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
61 import org.whispersystems.signalservice.api.messages.*;
62 import org.whispersystems.signalservice.api.messages.multidevice.*;
63 import org.whispersystems.signalservice.api.push.ContactTokenDetails;
64 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
65 import org.whispersystems.signalservice.api.push.TrustStore;
66 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
67 import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
68 import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
69 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
70 import org.whispersystems.signalservice.api.util.InvalidNumberException;
71 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
72 import org.whispersystems.signalservice.api.util.SleepTimer;
73 import org.whispersystems.signalservice.api.util.UptimeSleepTimer;
74 import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl;
75 import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl;
76 import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration;
77 import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl;
78 import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
79 import org.whispersystems.signalservice.internal.util.Base64;
80
81 import java.io.*;
82 import java.net.URI;
83 import java.net.URISyntaxException;
84 import java.net.URLDecoder;
85 import java.net.URLEncoder;
86 import java.nio.channels.Channels;
87 import java.nio.channels.FileChannel;
88 import java.nio.channels.FileLock;
89 import java.nio.file.Files;
90 import java.nio.file.Path;
91 import java.nio.file.Paths;
92 import java.nio.file.StandardCopyOption;
93 import java.nio.file.attribute.PosixFilePermission;
94 import java.nio.file.attribute.PosixFilePermissions;
95 import java.util.*;
96 import java.util.concurrent.TimeUnit;
97 import java.util.concurrent.TimeoutException;
98
99 import static java.nio.file.attribute.PosixFilePermission.*;
100
101 class Manager implements Signal {
102 private final static String URL = "https://textsecure-service.whispersystems.org";
103 private final static String CDN_URL = "https://cdn.signal.org";
104 private final static TrustStore TRUST_STORE = new WhisperTrustStore();
105 private final static SignalServiceConfiguration serviceConfiguration = new SignalServiceConfiguration(
106 new SignalServiceUrl[]{new SignalServiceUrl(URL, TRUST_STORE)},
107 new SignalCdnUrl[]{new SignalCdnUrl(CDN_URL, TRUST_STORE)},
108 new SignalContactDiscoveryUrl[0]
109 );
110 private final static String UNIDENTIFIED_SENDER_TRUST_ROOT = "BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF";
111
112 public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
113 public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
114 private final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION;
115
116 private final static int PREKEY_MINIMUM_COUNT = 20;
117 private static final int PREKEY_BATCH_SIZE = 100;
118 private static final int MAX_ATTACHMENT_SIZE = 150 * 1024 * 1024;
119
120 private final String settingsPath;
121 private final String dataPath;
122 private final String attachmentsPath;
123 private final String avatarsPath;
124
125 private FileChannel fileChannel;
126 private FileLock lock;
127
128 private final ObjectMapper jsonProcessor = new ObjectMapper();
129 private String username;
130 private int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
131 private boolean isMultiDevice = false;
132 private String password;
133 private String registrationLockPin;
134 private String signalingKey;
135 private byte[] profileKey;
136 private int preKeyIdOffset;
137 private int nextSignedPreKeyId;
138
139 private boolean registered = false;
140
141 private JsonSignalProtocolStore signalProtocolStore;
142 private SignalServiceAccountManager accountManager;
143 private JsonGroupStore groupStore;
144 private JsonContactsStore contactStore;
145 private JsonThreadStore threadStore;
146 private SignalServiceMessagePipe messagePipe = null;
147 private SignalServiceMessagePipe unidentifiedMessagePipe = null;
148
149 private SleepTimer timer = new UptimeSleepTimer();
150
151 public Manager(String username, String settingsPath) {
152 this.username = username;
153 this.settingsPath = settingsPath;
154 this.dataPath = this.settingsPath + "/data";
155 this.attachmentsPath = this.settingsPath + "/attachments";
156 this.avatarsPath = this.settingsPath + "/avatars";
157
158 jsonProcessor.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
159 jsonProcessor.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
160 jsonProcessor.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
161 jsonProcessor.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
162 jsonProcessor.disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
163 jsonProcessor.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
164 }
165
166 public String getUsername() {
167 return username;
168 }
169
170 private IdentityKey getIdentity() {
171 return signalProtocolStore.getIdentityKeyPair().getPublicKey();
172 }
173
174 public int getDeviceId() {
175 return deviceId;
176 }
177
178 public String getFileName() {
179 return dataPath + "/" + username;
180 }
181
182 private String getMessageCachePath() {
183 return this.dataPath + "/" + username + ".d/msg-cache";
184 }
185
186 private String getMessageCachePath(String sender) {
187 return getMessageCachePath() + "/" + sender.replace("/", "_");
188 }
189
190 private File getMessageCacheFile(String sender, long now, long timestamp) throws IOException {
191 String cachePath = getMessageCachePath(sender);
192 createPrivateDirectories(cachePath);
193 return new File(cachePath + "/" + now + "_" + timestamp);
194 }
195
196 private static void createPrivateDirectories(String path) throws IOException {
197 final Path file = new File(path).toPath();
198 try {
199 Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE, OWNER_EXECUTE);
200 Files.createDirectories(file, PosixFilePermissions.asFileAttribute(perms));
201 } catch (UnsupportedOperationException e) {
202 Files.createDirectories(file);
203 }
204 }
205
206 private static void createPrivateFile(String path) throws IOException {
207 final Path file = new File(path).toPath();
208 try {
209 Set<PosixFilePermission> perms = EnumSet.of(OWNER_READ, OWNER_WRITE);
210 Files.createFile(file, PosixFilePermissions.asFileAttribute(perms));
211 } catch (UnsupportedOperationException e) {
212 Files.createFile(file);
213 }
214 }
215
216 public boolean userExists() {
217 if (username == null) {
218 return false;
219 }
220 File f = new File(getFileName());
221 return !(!f.exists() || f.isDirectory());
222 }
223
224 public boolean userHasKeys() {
225 return signalProtocolStore != null;
226 }
227
228 private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
229 JsonNode node = parent.get(name);
230 if (node == null) {
231 throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
232 }
233
234 return node;
235 }
236
237 private void openFileChannel() throws IOException {
238 if (fileChannel != null)
239 return;
240
241 createPrivateDirectories(dataPath);
242 if (!new File(getFileName()).exists()) {
243 createPrivateFile(getFileName());
244 }
245 fileChannel = new RandomAccessFile(new File(getFileName()), "rw").getChannel();
246 lock = fileChannel.tryLock();
247 if (lock == null) {
248 System.err.println("Config file is in use by another instance, waiting…");
249 lock = fileChannel.lock();
250 System.err.println("Config file lock acquired.");
251 }
252 }
253
254 public void init() throws IOException {
255 load();
256
257 migrateLegacyConfigs();
258
259 accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, deviceId, USER_AGENT, timer);
260 try {
261 if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) {
262 refreshPreKeys();
263 save();
264 }
265 } catch (AuthorizationFailedException e) {
266 System.err.println("Authorization failed, was the number registered elsewhere?");
267 }
268 }
269
270 private void load() throws IOException {
271 openFileChannel();
272 JsonNode rootNode = jsonProcessor.readTree(Channels.newInputStream(fileChannel));
273
274 JsonNode node = rootNode.get("deviceId");
275 if (node != null) {
276 deviceId = node.asInt();
277 }
278 username = getNotNullNode(rootNode, "username").asText();
279 password = getNotNullNode(rootNode, "password").asText();
280 JsonNode pinNode = rootNode.get("registrationLockPin");
281 registrationLockPin = pinNode == null ? null : pinNode.asText();
282 if (rootNode.has("signalingKey")) {
283 signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
284 }
285 if (rootNode.has("preKeyIdOffset")) {
286 preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
287 } else {
288 preKeyIdOffset = 0;
289 }
290 if (rootNode.has("nextSignedPreKeyId")) {
291 nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
292 } else {
293 nextSignedPreKeyId = 0;
294 }
295 if (rootNode.has("profileKey")) {
296 profileKey = Base64.decode(getNotNullNode(rootNode, "profileKey").asText());
297 } else {
298 // Old config file, creating new profile key
299 profileKey = KeyUtils.createProfileKey();
300 }
301
302 signalProtocolStore = jsonProcessor.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
303 registered = getNotNullNode(rootNode, "registered").asBoolean();
304 JsonNode groupStoreNode = rootNode.get("groupStore");
305 if (groupStoreNode != null) {
306 groupStore = jsonProcessor.convertValue(groupStoreNode, JsonGroupStore.class);
307 }
308 if (groupStore == null) {
309 groupStore = new JsonGroupStore();
310 }
311
312 JsonNode contactStoreNode = rootNode.get("contactStore");
313 if (contactStoreNode != null) {
314 contactStore = jsonProcessor.convertValue(contactStoreNode, JsonContactsStore.class);
315 }
316 if (contactStore == null) {
317 contactStore = new JsonContactsStore();
318 }
319 JsonNode threadStoreNode = rootNode.get("threadStore");
320 if (threadStoreNode != null) {
321 threadStore = jsonProcessor.convertValue(threadStoreNode, JsonThreadStore.class);
322 }
323 if (threadStore == null) {
324 threadStore = new JsonThreadStore();
325 }
326 }
327
328 private void migrateLegacyConfigs() {
329 // Copy group avatars that were previously stored in the attachments folder
330 // to the new avatar folder
331 if (JsonGroupStore.groupsWithLegacyAvatarId.size() > 0) {
332 for (GroupInfo g : JsonGroupStore.groupsWithLegacyAvatarId) {
333 File avatarFile = getGroupAvatarFile(g.groupId);
334 File attachmentFile = getAttachmentFile(g.getAvatarId());
335 if (!avatarFile.exists() && attachmentFile.exists()) {
336 try {
337 createPrivateDirectories(avatarsPath);
338 Files.copy(attachmentFile.toPath(), avatarFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
339 } catch (Exception e) {
340 // Ignore
341 }
342 }
343 }
344 JsonGroupStore.groupsWithLegacyAvatarId.clear();
345 save();
346 }
347 }
348
349 private void save() {
350 if (username == null) {
351 return;
352 }
353 ObjectNode rootNode = jsonProcessor.createObjectNode();
354 rootNode.put("username", username)
355 .put("deviceId", deviceId)
356 .put("password", password)
357 .put("registrationLockPin", registrationLockPin)
358 .put("signalingKey", signalingKey)
359 .put("preKeyIdOffset", preKeyIdOffset)
360 .put("nextSignedPreKeyId", nextSignedPreKeyId)
361 .put("registered", registered)
362 .putPOJO("axolotlStore", signalProtocolStore)
363 .putPOJO("groupStore", groupStore)
364 .putPOJO("contactStore", contactStore)
365 .putPOJO("threadStore", threadStore)
366 ;
367 try {
368 openFileChannel();
369 fileChannel.position(0);
370 jsonProcessor.writeValue(Channels.newOutputStream(fileChannel), rootNode);
371 fileChannel.truncate(fileChannel.position());
372 fileChannel.force(false);
373 } catch (Exception e) {
374 System.err.println(String.format("Error saving file: %s", e.getMessage()));
375 }
376 }
377
378 public void createNewIdentity() {
379 IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
380 int registrationId = KeyHelper.generateRegistrationId(false);
381 signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
382 groupStore = new JsonGroupStore();
383 registered = false;
384 save();
385 }
386
387 public boolean isRegistered() {
388 return registered;
389 }
390
391 public void register(boolean voiceVerification) throws IOException {
392 password = KeyUtils.createPassword();
393
394 accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT, timer);
395
396 if (voiceVerification)
397 accountManager.requestVoiceVerificationCode();
398 else
399 accountManager.requestSmsVerificationCode();
400
401 registered = false;
402 save();
403 }
404
405 public void updateAccountAttributes() throws IOException {
406 accountManager.setAccountAttributes(signalingKey, signalProtocolStore.getLocalRegistrationId(), true, registrationLockPin, getSelfUnidentifiedAccessKey(), false);
407 }
408
409 public void unregister() throws IOException {
410 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
411 // If this is the master device, other users can't send messages to this number anymore.
412 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
413 accountManager.setGcmId(Optional.<String>absent());
414 }
415
416 public URI getDeviceLinkUri() throws TimeoutException, IOException {
417 password = KeyUtils.createPassword();
418
419 accountManager = new SignalServiceAccountManager(serviceConfiguration, username, password, USER_AGENT, timer);
420 String uuid = accountManager.getNewDeviceUuid();
421
422 registered = false;
423 try {
424 return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8"));
425 } catch (URISyntaxException e) {
426 // Shouldn't happen
427 return null;
428 }
429 }
430
431 public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists {
432 signalingKey = KeyUtils.createSignalingKey();
433 SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName);
434 deviceId = ret.getDeviceId();
435 username = ret.getNumber();
436 // TODO do this check before actually registering
437 if (userExists()) {
438 throw new UserAlreadyExists(username, getFileName());
439 }
440 signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId());
441
442 registered = true;
443 isMultiDevice = true;
444 refreshPreKeys();
445
446 requestSyncGroups();
447 requestSyncContacts();
448
449 save();
450 }
451
452 public List<DeviceInfo> getLinkedDevices() throws IOException {
453 List<DeviceInfo> devices = accountManager.getDevices();
454 isMultiDevice = devices.size() > 1;
455 return devices;
456 }
457
458 public void removeLinkedDevices(int deviceId) throws IOException {
459 accountManager.removeDevice(deviceId);
460 }
461
462 public static Map<String, String> getQueryMap(String query) {
463 String[] params = query.split("&");
464 Map<String, String> map = new HashMap<>();
465 for (String param : params) {
466 String name = null;
467 final String[] paramParts = param.split("=");
468 try {
469 name = URLDecoder.decode(paramParts[0], "utf-8");
470 } catch (UnsupportedEncodingException e) {
471 // Impossible
472 }
473 String value = null;
474 try {
475 value = URLDecoder.decode(paramParts[1], "utf-8");
476 } catch (UnsupportedEncodingException e) {
477 // Impossible
478 }
479 map.put(name, value);
480 }
481 return map;
482 }
483
484 public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
485 Map<String, String> query = getQueryMap(linkUri.getRawQuery());
486 String deviceIdentifier = query.get("uuid");
487 String publicKeyEncoded = query.get("pub_key");
488
489 if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) {
490 throw new RuntimeException("Invalid device link uri");
491 }
492
493 ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
494
495 addDevice(deviceIdentifier, deviceKey);
496 }
497
498 private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
499 IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair();
500 String verificationCode = accountManager.getNewDeviceVerificationCode();
501
502 accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(profileKey), verificationCode);
503 isMultiDevice = true;
504 }
505
506 private List<PreKeyRecord> generatePreKeys() {
507 List<PreKeyRecord> records = new LinkedList<>();
508
509 for (int i = 0; i < PREKEY_BATCH_SIZE; i++) {
510 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
511 ECKeyPair keyPair = Curve.generateKeyPair();
512 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
513
514 signalProtocolStore.storePreKey(preKeyId, record);
515 records.add(record);
516 }
517
518 preKeyIdOffset = (preKeyIdOffset + PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE;
519 save();
520
521 return records;
522 }
523
524 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
525 try {
526 ECKeyPair keyPair = Curve.generateKeyPair();
527 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
528 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
529
530 signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record);
531 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
532 save();
533
534 return record;
535 } catch (InvalidKeyException e) {
536 throw new AssertionError(e);
537 }
538 }
539
540 public void verifyAccount(String verificationCode, String pin) throws IOException {
541 verificationCode = verificationCode.replace("-", "");
542 signalingKey = KeyUtils.createSignalingKey();
543 accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), true, pin, getSelfUnidentifiedAccessKey(), false);
544
545 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
546 registered = true;
547 registrationLockPin = pin;
548
549 refreshPreKeys();
550 save();
551 }
552
553 public void setRegistrationLockPin(Optional<String> pin) throws IOException {
554 accountManager.setPin(pin);
555 if (pin.isPresent()) {
556 registrationLockPin = pin.get();
557 } else {
558 registrationLockPin = null;
559 }
560 }
561
562 private void refreshPreKeys() throws IOException {
563 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
564 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair());
565
566 accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), signedPreKeyRecord, oneTimePreKeys);
567 }
568
569
570 private static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
571 List<SignalServiceAttachment> SignalServiceAttachments = null;
572 if (attachments != null) {
573 SignalServiceAttachments = new ArrayList<>(attachments.size());
574 for (String attachment : attachments) {
575 try {
576 SignalServiceAttachments.add(createAttachment(new File(attachment)));
577 } catch (IOException e) {
578 throw new AttachmentInvalidException(attachment, e);
579 }
580 }
581 }
582 return SignalServiceAttachments;
583 }
584
585 private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
586 InputStream attachmentStream = new FileInputStream(attachmentFile);
587 final long attachmentSize = attachmentFile.length();
588 String mime = Files.probeContentType(attachmentFile.toPath());
589 if (mime == null) {
590 mime = "application/octet-stream";
591 }
592 // TODO mabybe add a parameter to set the voiceNote, preview, width, height and caption option
593 Optional<byte[]> preview = Optional.absent();
594 Optional<String> caption = Optional.absent();
595 return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), false, preview, 0, 0, caption, null);
596 }
597
598 private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException {
599 File file = getGroupAvatarFile(groupId);
600 if (!file.exists()) {
601 return Optional.absent();
602 }
603
604 return Optional.of(createAttachment(file));
605 }
606
607 private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(String number) throws IOException {
608 File file = getContactAvatarFile(number);
609 if (!file.exists()) {
610 return Optional.absent();
611 }
612
613 return Optional.of(createAttachment(file));
614 }
615
616 private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException {
617 GroupInfo g = groupStore.getGroup(groupId);
618 if (g == null) {
619 throw new GroupNotFoundException(groupId);
620 }
621 for (String member : g.members) {
622 if (member.equals(this.username)) {
623 return g;
624 }
625 }
626 throw new NotAGroupMemberException(groupId, g.name);
627 }
628
629 public List<GroupInfo> getGroups() {
630 return groupStore.getGroups();
631 }
632
633 @Override
634 public void sendGroupMessage(String messageText, List<String> attachments,
635 byte[] groupId)
636 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
637 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
638 if (attachments != null) {
639 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
640 }
641 if (groupId != null) {
642 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
643 .withId(groupId)
644 .build();
645 messageBuilder.asGroupMessage(group);
646 }
647 ThreadInfo thread = threadStore.getThread(Base64.encodeBytes(groupId));
648 if (thread != null) {
649 messageBuilder.withExpiration(thread.messageExpirationTime);
650 }
651
652 final GroupInfo g = getGroupForSending(groupId);
653
654 // Don't send group message to ourself
655 final List<String> membersSend = new ArrayList<>(g.members);
656 membersSend.remove(this.username);
657 sendMessageLegacy(messageBuilder, membersSend);
658 }
659
660 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
661 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
662 .withId(groupId)
663 .build();
664
665 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
666 .asGroupMessage(group);
667
668 final GroupInfo g = getGroupForSending(groupId);
669 g.members.remove(this.username);
670 groupStore.updateGroup(g);
671
672 sendMessageLegacy(messageBuilder, g.members);
673 }
674
675 private static String join(CharSequence separator, Iterable<? extends CharSequence> list) {
676 StringBuilder buf = new StringBuilder();
677 for (CharSequence str : list) {
678 if (buf.length() > 0) {
679 buf.append(separator);
680 }
681 buf.append(str);
682 }
683
684 return buf.toString();
685 }
686
687 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
688 GroupInfo g;
689 if (groupId == null) {
690 // Create new group
691 g = new GroupInfo(KeyUtils.createGroupId());
692 g.members.add(username);
693 } else {
694 g = getGroupForSending(groupId);
695 }
696
697 if (name != null) {
698 g.name = name;
699 }
700
701 if (members != null) {
702 Set<String> newMembers = new HashSet<>();
703 for (String member : members) {
704 try {
705 member = canonicalizeNumber(member);
706 } catch (InvalidNumberException e) {
707 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
708 System.err.println("Aborting…");
709 System.exit(1);
710 }
711 if (g.members.contains(member)) {
712 continue;
713 }
714 newMembers.add(member);
715 g.members.add(member);
716 }
717 final List<ContactTokenDetails> contacts = accountManager.getContacts(newMembers);
718 if (contacts.size() != newMembers.size()) {
719 // Some of the new members are not registered on Signal
720 for (ContactTokenDetails contact : contacts) {
721 newMembers.remove(contact.getNumber());
722 }
723 System.err.println("Failed to add members " + join(", ", newMembers) + " to group: Not registered on Signal");
724 System.err.println("Aborting…");
725 System.exit(1);
726 }
727 }
728
729 if (avatarFile != null) {
730 createPrivateDirectories(avatarsPath);
731 File aFile = getGroupAvatarFile(g.groupId);
732 Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
733 }
734
735 groupStore.updateGroup(g);
736
737 SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
738
739 // Don't send group message to ourself
740 final List<String> membersSend = new ArrayList<>(g.members);
741 membersSend.remove(this.username);
742 sendMessageLegacy(messageBuilder, membersSend);
743 return g.groupId;
744 }
745
746 private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
747 if (groupId == null) {
748 return;
749 }
750 GroupInfo g = getGroupForSending(groupId);
751
752 if (!g.members.contains(recipient)) {
753 return;
754 }
755
756 SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
757
758 // Send group message only to the recipient who requested it
759 final List<String> membersSend = new ArrayList<>();
760 membersSend.add(recipient);
761 sendMessageLegacy(messageBuilder, membersSend);
762 }
763
764 private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) {
765 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
766 .withId(g.groupId)
767 .withName(g.name)
768 .withMembers(new ArrayList<>(g.members));
769
770 File aFile = getGroupAvatarFile(g.groupId);
771 if (aFile.exists()) {
772 try {
773 group.withAvatar(createAttachment(aFile));
774 } catch (IOException e) {
775 throw new AttachmentInvalidException(aFile.toString(), e);
776 }
777 }
778
779 return SignalServiceDataMessage.newBuilder()
780 .asGroupMessage(group.build());
781 }
782
783 private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
784 if (groupId == null) {
785 return;
786 }
787
788 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO)
789 .withId(groupId);
790
791 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
792 .asGroupMessage(group.build());
793
794 // Send group info request message to the recipient who sent us a message with this groupId
795 final List<String> membersSend = new ArrayList<>();
796 membersSend.add(recipient);
797 sendMessageLegacy(messageBuilder, membersSend);
798 }
799
800 @Override
801 public void sendMessage(String message, List<String> attachments, String recipient)
802 throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
803 List<String> recipients = new ArrayList<>(1);
804 recipients.add(recipient);
805 sendMessage(message, attachments, recipients);
806 }
807
808 @Override
809 public void sendMessage(String messageText, List<String> attachments,
810 List<String> recipients)
811 throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
812 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
813 if (attachments != null) {
814 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
815 }
816 sendMessageLegacy(messageBuilder, recipients);
817 }
818
819 @Override
820 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
821 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
822 .asEndSessionMessage();
823
824 sendMessageLegacy(messageBuilder, recipients);
825 }
826
827 @Override
828 public String getContactName(String number) {
829 ContactInfo contact = contactStore.getContact(number);
830 if (contact == null) {
831 return "";
832 } else {
833 return contact.name;
834 }
835 }
836
837 @Override
838 public void setContactName(String number, String name) {
839 ContactInfo contact = contactStore.getContact(number);
840 if (contact == null) {
841 contact = new ContactInfo();
842 contact.number = number;
843 System.err.println("Add contact " + number + " named " + name);
844 } else {
845 System.err.println("Updating contact " + number + " name " + contact.name + " -> " + name);
846 }
847 contact.name = name;
848 contactStore.updateContact(contact);
849 save();
850 }
851
852 @Override
853 public List<byte[]> getGroupIds() {
854 List<GroupInfo> groups = getGroups();
855 List<byte[]> ids = new ArrayList<byte[]>(groups.size());
856 for (GroupInfo group : groups) {
857 ids.add(group.groupId);
858 }
859 return ids;
860 }
861
862 @Override
863 public String getGroupName(byte[] groupId) {
864 GroupInfo group = getGroup(groupId);
865 if (group == null) {
866 return "";
867 } else {
868 return group.name;
869 }
870 }
871
872 @Override
873 public List<String> getGroupMembers(byte[] groupId) {
874 GroupInfo group = getGroup(groupId);
875 if (group == null) {
876 return new ArrayList<String>();
877 } else {
878 return new ArrayList<String>(group.members);
879 }
880 }
881
882 @Override
883 public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
884 if (groupId.length == 0) {
885 groupId = null;
886 }
887 if (name.isEmpty()) {
888 name = null;
889 }
890 if (members.size() == 0) {
891 members = null;
892 }
893 if (avatar.isEmpty()) {
894 avatar = null;
895 }
896 return sendUpdateGroupMessage(groupId, name, members, avatar);
897 }
898
899 private void requestSyncGroups() throws IOException {
900 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
901 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
902 try {
903 sendSyncMessage(message);
904 } catch (UntrustedIdentityException e) {
905 e.printStackTrace();
906 }
907 }
908
909 private void requestSyncContacts() throws IOException {
910 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
911 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
912 try {
913 sendSyncMessage(message);
914 } catch (UntrustedIdentityException e) {
915 e.printStackTrace();
916 }
917 }
918
919 private byte[] getSelfUnidentifiedAccessKey() {
920 return UnidentifiedAccess.deriveAccessKeyFrom(profileKey);
921 }
922
923 private static byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
924 // TODO implement
925 return null;
926 }
927
928 public Optional<UnidentifiedAccessPair> getAccessForSync() {
929 // TODO implement
930 return Optional.absent();
931 }
932
933 public List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<SignalServiceAddress> recipients) {
934 List<Optional<UnidentifiedAccessPair>> result = new ArrayList<>(recipients.size());
935 for (SignalServiceAddress recipient : recipients) {
936 result.add(Optional.<UnidentifiedAccessPair>absent());
937 }
938 return result;
939 }
940
941 public Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress recipient) {
942 // TODO implement
943 return Optional.absent();
944 }
945
946 private void sendSyncMessage(SignalServiceSyncMessage message)
947 throws IOException, UntrustedIdentityException {
948 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password,
949 deviceId, signalProtocolStore, USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
950 try {
951 messageSender.sendMessage(message, getAccessForSync());
952 } catch (UntrustedIdentityException e) {
953 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
954 throw e;
955 }
956 }
957
958 /**
959 * This method throws an EncapsulatedExceptions exception instead of returning a list of SendMessageResult.
960 */
961 private void sendMessageLegacy(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
962 throws EncapsulatedExceptions, IOException {
963 List<SendMessageResult> results = sendMessage(messageBuilder, recipients);
964
965 List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
966 List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
967 List<NetworkFailureException> networkExceptions = new LinkedList<>();
968
969 for (SendMessageResult result : results) {
970 if (result.isUnregisteredFailure()) {
971 unregisteredUsers.add(new UnregisteredUserException(result.getAddress().getNumber(), null));
972 } else if (result.isNetworkFailure()) {
973 networkExceptions.add(new NetworkFailureException(result.getAddress().getNumber(), null));
974 } else if (result.getIdentityFailure() != null) {
975 untrustedIdentities.add(new UntrustedIdentityException("Untrusted", result.getAddress().getNumber(), result.getIdentityFailure().getIdentityKey()));
976 }
977 }
978 if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
979 throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
980 }
981 }
982
983 private List<SendMessageResult> sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
984 throws IOException {
985 Set<SignalServiceAddress> recipientsTS = getSignalServiceAddresses(recipients);
986 if (recipientsTS == null) return Collections.emptyList();
987
988 SignalServiceDataMessage message = null;
989 try {
990 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceConfiguration, username, password,
991 deviceId, signalProtocolStore, USER_AGENT, isMultiDevice, Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
992
993 message = messageBuilder.build();
994 if (message.getGroupInfo().isPresent()) {
995 try {
996 List<SendMessageResult> result = messageSender.sendMessage(new ArrayList<>(recipientsTS), getAccessFor(recipientsTS), message);
997 for (SendMessageResult r : result) {
998 if (r.getIdentityFailure() != null) {
999 signalProtocolStore.saveIdentity(r.getAddress().getNumber(), r.getIdentityFailure().getIdentityKey(), TrustLevel.UNTRUSTED);
1000 }
1001 }
1002 return result;
1003 } catch (UntrustedIdentityException e) {
1004 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
1005 return Collections.emptyList();
1006 }
1007 } else {
1008 // Send to all individually, so sync messages are sent correctly
1009 List<SendMessageResult> results = new ArrayList<>(recipientsTS.size());
1010 for (SignalServiceAddress address : recipientsTS) {
1011 ThreadInfo thread = threadStore.getThread(address.getNumber());
1012 if (thread != null) {
1013 messageBuilder.withExpiration(thread.messageExpirationTime);
1014 } else {
1015 messageBuilder.withExpiration(0);
1016 }
1017 message = messageBuilder.build();
1018 try {
1019 SendMessageResult result = messageSender.sendMessage(address, getAccessFor(address), message);
1020 results.add(result);
1021 } catch (UntrustedIdentityException e) {
1022 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
1023 results.add(SendMessageResult.identityFailure(address, e.getIdentityKey()));
1024 }
1025 }
1026 return results;
1027 }
1028 } finally {
1029 if (message != null && message.isEndSession()) {
1030 for (SignalServiceAddress recipient : recipientsTS) {
1031 handleEndSession(recipient.getNumber());
1032 }
1033 }
1034 save();
1035 }
1036 }
1037
1038 private Set<SignalServiceAddress> getSignalServiceAddresses(Collection<String> recipients) {
1039 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
1040 for (String recipient : recipients) {
1041 try {
1042 recipientsTS.add(getPushAddress(recipient));
1043 } catch (InvalidNumberException e) {
1044 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
1045 System.err.println("Aborting sending.");
1046 save();
1047 return null;
1048 }
1049 }
1050 return recipientsTS;
1051 }
1052
1053 public static CertificateValidator getCertificateValidator() {
1054 try {
1055 ECPublicKey unidentifiedSenderTrustRoot = Curve.decodePoint(Base64.decode(UNIDENTIFIED_SENDER_TRUST_ROOT), 0);
1056 return new CertificateValidator(unidentifiedSenderTrustRoot);
1057 } catch (InvalidKeyException | IOException e) {
1058 throw new AssertionError(e);
1059 }
1060 }
1061
1062 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws org.whispersystems.libsignal.UntrustedIdentityException, InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, ProtocolUntrustedIdentityException, SelfSendException {
1063 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore, getCertificateValidator());
1064 try {
1065 return cipher.decrypt(envelope);
1066 } catch (ProtocolUntrustedIdentityException e) {
1067 // TODO We don't get the new untrusted identity from ProtocolUntrustedIdentityException anymore ... we need to get it from somewhere else
1068 // signalProtocolStore.saveIdentity(e.getSender(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
1069 throw e;
1070 }
1071 }
1072
1073 private void handleEndSession(String source) {
1074 signalProtocolStore.deleteAllSessions(source);
1075 }
1076
1077 public interface ReceiveMessageHandler {
1078 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e);
1079 }
1080
1081 private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination, boolean ignoreAttachments) {
1082 String threadId;
1083 if (message.getGroupInfo().isPresent()) {
1084 SignalServiceGroup groupInfo = message.getGroupInfo().get();
1085 threadId = Base64.encodeBytes(groupInfo.getGroupId());
1086 GroupInfo group = groupStore.getGroup(groupInfo.getGroupId());
1087 switch (groupInfo.getType()) {
1088 case UPDATE:
1089 if (group == null) {
1090 group = new GroupInfo(groupInfo.getGroupId());
1091 }
1092
1093 if (groupInfo.getAvatar().isPresent()) {
1094 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
1095 if (avatar.isPointer()) {
1096 try {
1097 retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
1098 } catch (IOException | InvalidMessageException e) {
1099 System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage());
1100 }
1101 }
1102 }
1103
1104 if (groupInfo.getName().isPresent()) {
1105 group.name = groupInfo.getName().get();
1106 }
1107
1108 if (groupInfo.getMembers().isPresent()) {
1109 group.members.addAll(groupInfo.getMembers().get());
1110 }
1111
1112 groupStore.updateGroup(group);
1113 break;
1114 case DELIVER:
1115 if (group == null) {
1116 try {
1117 sendGroupInfoRequest(groupInfo.getGroupId(), source);
1118 } catch (IOException | EncapsulatedExceptions e) {
1119 e.printStackTrace();
1120 }
1121 }
1122 break;
1123 case QUIT:
1124 if (group == null) {
1125 try {
1126 sendGroupInfoRequest(groupInfo.getGroupId(), source);
1127 } catch (IOException | EncapsulatedExceptions e) {
1128 e.printStackTrace();
1129 }
1130 } else {
1131 group.members.remove(source);
1132 groupStore.updateGroup(group);
1133 }
1134 break;
1135 case REQUEST_INFO:
1136 if (group != null) {
1137 try {
1138 sendUpdateGroupMessage(groupInfo.getGroupId(), source);
1139 } catch (IOException | EncapsulatedExceptions e) {
1140 e.printStackTrace();
1141 } catch (NotAGroupMemberException e) {
1142 // We have left this group, so don't send a group update message
1143 }
1144 }
1145 break;
1146 }
1147 } else {
1148 if (isSync) {
1149 threadId = destination;
1150 } else {
1151 threadId = source;
1152 }
1153 }
1154 if (message.isEndSession()) {
1155 handleEndSession(isSync ? destination : source);
1156 }
1157 if (message.isExpirationUpdate() || message.getBody().isPresent()) {
1158 ThreadInfo thread = threadStore.getThread(threadId);
1159 if (thread == null) {
1160 thread = new ThreadInfo();
1161 thread.id = threadId;
1162 }
1163 if (thread.messageExpirationTime != message.getExpiresInSeconds()) {
1164 thread.messageExpirationTime = message.getExpiresInSeconds();
1165 threadStore.updateThread(thread);
1166 }
1167 }
1168 if (message.getAttachments().isPresent() && !ignoreAttachments) {
1169 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
1170 if (attachment.isPointer()) {
1171 try {
1172 retrieveAttachment(attachment.asPointer());
1173 } catch (IOException | InvalidMessageException e) {
1174 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
1175 }
1176 }
1177 }
1178 }
1179 if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) {
1180 ContactInfo contact = contactStore.getContact(source);
1181 if (contact == null) {
1182 contact = new ContactInfo();
1183 contact.number = source;
1184 }
1185 contact.profileKey = Base64.encodeBytes(message.getProfileKey().get());
1186 }
1187 }
1188
1189 public void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
1190 final File cachePath = new File(getMessageCachePath());
1191 if (!cachePath.exists()) {
1192 return;
1193 }
1194 for (final File dir : cachePath.listFiles()) {
1195 if (!dir.isDirectory()) {
1196 continue;
1197 }
1198
1199 for (final File fileEntry : dir.listFiles()) {
1200 if (!fileEntry.isFile()) {
1201 continue;
1202 }
1203 SignalServiceEnvelope envelope;
1204 try {
1205 envelope = loadEnvelope(fileEntry);
1206 if (envelope == null) {
1207 continue;
1208 }
1209 } catch (IOException e) {
1210 e.printStackTrace();
1211 continue;
1212 }
1213 SignalServiceContent content = null;
1214 if (!envelope.isReceipt()) {
1215 try {
1216 content = decryptMessage(envelope);
1217 } catch (Exception e) {
1218 continue;
1219 }
1220 handleMessage(envelope, content, ignoreAttachments);
1221 }
1222 save();
1223 handler.handleMessage(envelope, content, null);
1224 try {
1225 Files.delete(fileEntry.toPath());
1226 } catch (IOException e) {
1227 System.err.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
1228 }
1229 }
1230 // Try to delete directory if empty
1231 dir.delete();
1232 }
1233 }
1234
1235 public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException {
1236 retryFailedReceivedMessages(handler, ignoreAttachments);
1237 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer);
1238
1239 try {
1240 if (messagePipe == null) {
1241 messagePipe = messageReceiver.createMessagePipe();
1242 }
1243
1244 while (true) {
1245 SignalServiceEnvelope envelope;
1246 SignalServiceContent content = null;
1247 Exception exception = null;
1248 final long now = new Date().getTime();
1249 try {
1250 envelope = messagePipe.read(timeout, unit, new SignalServiceMessagePipe.MessagePipeCallback() {
1251 @Override
1252 public void onMessage(SignalServiceEnvelope envelope) {
1253 // store message on disk, before acknowledging receipt to the server
1254 try {
1255 File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1256 storeEnvelope(envelope, cacheFile);
1257 } catch (IOException e) {
1258 System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
1259 }
1260 }
1261 });
1262 } catch (TimeoutException e) {
1263 if (returnOnTimeout)
1264 return;
1265 continue;
1266 } catch (InvalidVersionException e) {
1267 System.err.println("Ignoring error: " + e.getMessage());
1268 continue;
1269 }
1270 if (!envelope.isReceipt()) {
1271 try {
1272 content = decryptMessage(envelope);
1273 } catch (Exception e) {
1274 exception = e;
1275 }
1276 handleMessage(envelope, content, ignoreAttachments);
1277 }
1278 save();
1279 handler.handleMessage(envelope, content, exception);
1280 if (exception == null || !(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
1281 File cacheFile = null;
1282 try {
1283 cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1284 Files.delete(cacheFile.toPath());
1285 // Try to delete directory if empty
1286 new File(getMessageCachePath()).delete();
1287 } catch (IOException e) {
1288 System.err.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
1289 }
1290 }
1291 }
1292 } finally {
1293 if (messagePipe != null) {
1294 messagePipe.shutdown();
1295 messagePipe = null;
1296 }
1297 }
1298 }
1299
1300 private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
1301 if (content != null) {
1302 if (content.getDataMessage().isPresent()) {
1303 SignalServiceDataMessage message = content.getDataMessage().get();
1304 handleSignalServiceDataMessage(message, false, envelope.getSource(), username, ignoreAttachments);
1305 }
1306 if (content.getSyncMessage().isPresent()) {
1307 isMultiDevice = true;
1308 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
1309 if (syncMessage.getSent().isPresent()) {
1310 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
1311 handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get(), ignoreAttachments);
1312 }
1313 if (syncMessage.getRequest().isPresent()) {
1314 RequestMessage rm = syncMessage.getRequest().get();
1315 if (rm.isContactsRequest()) {
1316 try {
1317 sendContacts();
1318 } catch (UntrustedIdentityException | IOException e) {
1319 e.printStackTrace();
1320 }
1321 }
1322 if (rm.isGroupsRequest()) {
1323 try {
1324 sendGroups();
1325 } catch (UntrustedIdentityException | IOException e) {
1326 e.printStackTrace();
1327 }
1328 }
1329 }
1330 if (syncMessage.getGroups().isPresent()) {
1331 File tmpFile = null;
1332 try {
1333 tmpFile = Util.createTempFile();
1334 try (InputStream attachmentAsStream = retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile)) {
1335 DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream);
1336 DeviceGroup g;
1337 while ((g = s.read()) != null) {
1338 GroupInfo syncGroup = groupStore.getGroup(g.getId());
1339 if (syncGroup == null) {
1340 syncGroup = new GroupInfo(g.getId());
1341 }
1342 if (g.getName().isPresent()) {
1343 syncGroup.name = g.getName().get();
1344 }
1345 syncGroup.members.addAll(g.getMembers());
1346 syncGroup.active = g.isActive();
1347 if (g.getColor().isPresent()) {
1348 syncGroup.color = g.getColor().get();
1349 }
1350
1351 if (g.getAvatar().isPresent()) {
1352 retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
1353 }
1354 groupStore.updateGroup(syncGroup);
1355 }
1356 }
1357 } catch (Exception e) {
1358 e.printStackTrace();
1359 } finally {
1360 if (tmpFile != null) {
1361 try {
1362 Files.delete(tmpFile.toPath());
1363 } catch (IOException e) {
1364 System.err.println("Failed to delete received groups temp file “" + tmpFile + "”: " + e.getMessage());
1365 }
1366 }
1367 }
1368 if (syncMessage.getBlockedList().isPresent()) {
1369 // TODO store list of blocked numbers
1370 }
1371 }
1372 if (syncMessage.getContacts().isPresent()) {
1373 File tmpFile = null;
1374 try {
1375 tmpFile = Util.createTempFile();
1376 final ContactsMessage contactsMessage = syncMessage.getContacts().get();
1377 try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream().asPointer(), tmpFile)) {
1378 DeviceContactsInputStream s = new DeviceContactsInputStream(attachmentAsStream);
1379 if (contactsMessage.isComplete()) {
1380 contactStore.clear();
1381 }
1382 DeviceContact c;
1383 while ((c = s.read()) != null) {
1384 ContactInfo contact = contactStore.getContact(c.getNumber());
1385 if (contact == null) {
1386 contact = new ContactInfo();
1387 contact.number = c.getNumber();
1388 }
1389 if (c.getName().isPresent()) {
1390 contact.name = c.getName().get();
1391 }
1392 if (c.getColor().isPresent()) {
1393 contact.color = c.getColor().get();
1394 }
1395 if (c.getProfileKey().isPresent()) {
1396 contact.profileKey = Base64.encodeBytes(c.getProfileKey().get());
1397 }
1398 if (c.getVerified().isPresent()) {
1399 final VerifiedMessage verifiedMessage = c.getVerified().get();
1400 signalProtocolStore.saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
1401 }
1402 if (c.getExpirationTimer().isPresent()) {
1403 ThreadInfo thread = threadStore.getThread(c.getNumber());
1404 thread.messageExpirationTime = c.getExpirationTimer().get();
1405 threadStore.updateThread(thread);
1406 }
1407 if (c.isBlocked()) {
1408 // TODO store list of blocked numbers
1409 }
1410 contactStore.updateContact(contact);
1411
1412 if (c.getAvatar().isPresent()) {
1413 retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
1414 }
1415 }
1416 }
1417 } catch (Exception e) {
1418 e.printStackTrace();
1419 } finally {
1420 if (tmpFile != null) {
1421 try {
1422 Files.delete(tmpFile.toPath());
1423 } catch (IOException e) {
1424 System.err.println("Failed to delete received contacts temp file “" + tmpFile + "”: " + e.getMessage());
1425 }
1426 }
1427 }
1428 }
1429 if (syncMessage.getVerified().isPresent()) {
1430 final VerifiedMessage verifiedMessage = syncMessage.getVerified().get();
1431 signalProtocolStore.saveIdentity(verifiedMessage.getDestination(), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
1432 }
1433 }
1434 }
1435 }
1436
1437 private SignalServiceEnvelope loadEnvelope(File file) throws IOException {
1438 try (FileInputStream f = new FileInputStream(file)) {
1439 DataInputStream in = new DataInputStream(f);
1440 int version = in.readInt();
1441 if (version > 2) {
1442 return null;
1443 }
1444 int type = in.readInt();
1445 String source = in.readUTF();
1446 int sourceDevice = in.readInt();
1447 if (version == 1) {
1448 // read legacy relay field
1449 in.readUTF();
1450 }
1451 long timestamp = in.readLong();
1452 byte[] content = null;
1453 int contentLen = in.readInt();
1454 if (contentLen > 0) {
1455 content = new byte[contentLen];
1456 in.readFully(content);
1457 }
1458 byte[] legacyMessage = null;
1459 int legacyMessageLen = in.readInt();
1460 if (legacyMessageLen > 0) {
1461 legacyMessage = new byte[legacyMessageLen];
1462 in.readFully(legacyMessage);
1463 }
1464 long serverTimestamp = 0;
1465 String uuid = null;
1466 if (version == 2) {
1467 serverTimestamp = in.readLong();
1468 uuid = in.readUTF();
1469 if ("".equals(uuid)) {
1470 uuid = null;
1471 }
1472 }
1473 return new SignalServiceEnvelope(type, source, sourceDevice, timestamp, legacyMessage, content, serverTimestamp, uuid);
1474 }
1475 }
1476
1477 private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
1478 try (FileOutputStream f = new FileOutputStream(file)) {
1479 try (DataOutputStream out = new DataOutputStream(f)) {
1480 out.writeInt(2); // version
1481 out.writeInt(envelope.getType());
1482 out.writeUTF(envelope.getSource());
1483 out.writeInt(envelope.getSourceDevice());
1484 out.writeLong(envelope.getTimestamp());
1485 if (envelope.hasContent()) {
1486 out.writeInt(envelope.getContent().length);
1487 out.write(envelope.getContent());
1488 } else {
1489 out.writeInt(0);
1490 }
1491 if (envelope.hasLegacyMessage()) {
1492 out.writeInt(envelope.getLegacyMessage().length);
1493 out.write(envelope.getLegacyMessage());
1494 } else {
1495 out.writeInt(0);
1496 }
1497 out.writeLong(envelope.getServerTimestamp());
1498 String uuid = envelope.getUuid();
1499 out.writeUTF(uuid == null ? "" : uuid);
1500 }
1501 }
1502 }
1503
1504 public File getContactAvatarFile(String number) {
1505 return new File(avatarsPath, "contact-" + number);
1506 }
1507
1508 private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
1509 createPrivateDirectories(avatarsPath);
1510 if (attachment.isPointer()) {
1511 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1512 return retrieveAttachment(pointer, getContactAvatarFile(number), false);
1513 } else {
1514 SignalServiceAttachmentStream stream = attachment.asStream();
1515 return retrieveAttachment(stream, getContactAvatarFile(number));
1516 }
1517 }
1518
1519 public File getGroupAvatarFile(byte[] groupId) {
1520 return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
1521 }
1522
1523 private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
1524 createPrivateDirectories(avatarsPath);
1525 if (attachment.isPointer()) {
1526 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1527 return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
1528 } else {
1529 SignalServiceAttachmentStream stream = attachment.asStream();
1530 return retrieveAttachment(stream, getGroupAvatarFile(groupId));
1531 }
1532 }
1533
1534 public File getAttachmentFile(long attachmentId) {
1535 return new File(attachmentsPath, attachmentId + "");
1536 }
1537
1538 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1539 createPrivateDirectories(attachmentsPath);
1540 return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
1541 }
1542
1543 private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
1544 InputStream input = stream.getInputStream();
1545
1546 try (OutputStream output = new FileOutputStream(outputFile)) {
1547 byte[] buffer = new byte[4096];
1548 int read;
1549
1550 while ((read = input.read(buffer)) != -1) {
1551 output.write(buffer, 0, read);
1552 }
1553 } catch (FileNotFoundException e) {
1554 e.printStackTrace();
1555 return null;
1556 }
1557 return outputFile;
1558 }
1559
1560 private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
1561 if (storePreview && pointer.getPreview().isPresent()) {
1562 File previewFile = new File(outputFile + ".preview");
1563 try (OutputStream output = new FileOutputStream(previewFile)) {
1564 byte[] preview = pointer.getPreview().get();
1565 output.write(preview, 0, preview.length);
1566 } catch (FileNotFoundException e) {
1567 e.printStackTrace();
1568 return null;
1569 }
1570 }
1571
1572 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer);
1573
1574 File tmpFile = Util.createTempFile();
1575 try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE)) {
1576 try (OutputStream output = new FileOutputStream(outputFile)) {
1577 byte[] buffer = new byte[4096];
1578 int read;
1579
1580 while ((read = input.read(buffer)) != -1) {
1581 output.write(buffer, 0, read);
1582 }
1583 } catch (FileNotFoundException e) {
1584 e.printStackTrace();
1585 return null;
1586 }
1587 } finally {
1588 try {
1589 Files.delete(tmpFile.toPath());
1590 } catch (IOException e) {
1591 System.err.println("Failed to delete received attachment temp file “" + tmpFile + "”: " + e.getMessage());
1592 }
1593 }
1594 return outputFile;
1595 }
1596
1597 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException {
1598 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceConfiguration, username, password, deviceId, signalingKey, USER_AGENT, null, timer);
1599 return messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE);
1600 }
1601
1602 private String canonicalizeNumber(String number) throws InvalidNumberException {
1603 String localNumber = username;
1604 return PhoneNumberFormatter.formatNumber(number, localNumber);
1605 }
1606
1607 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
1608 String e164number = canonicalizeNumber(number);
1609 return new SignalServiceAddress(e164number);
1610 }
1611
1612 @Override
1613 public boolean isRemote() {
1614 return false;
1615 }
1616
1617 private void sendGroups() throws IOException, UntrustedIdentityException {
1618 File groupsFile = Util.createTempFile();
1619
1620 try {
1621 try (OutputStream fos = new FileOutputStream(groupsFile)) {
1622 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
1623 for (GroupInfo record : groupStore.getGroups()) {
1624 ThreadInfo info = threadStore.getThread(Base64.encodeBytes(record.groupId));
1625 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
1626 new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
1627 record.active, Optional.fromNullable(info != null ? info.messageExpirationTime : null),
1628 Optional.fromNullable(record.color), false));
1629 }
1630 }
1631
1632 if (groupsFile.exists() && groupsFile.length() > 0) {
1633 try (FileInputStream groupsFileStream = new FileInputStream(groupsFile)) {
1634 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1635 .withStream(groupsFileStream)
1636 .withContentType("application/octet-stream")
1637 .withLength(groupsFile.length())
1638 .build();
1639
1640 sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
1641 }
1642 }
1643 } finally {
1644 try {
1645 Files.delete(groupsFile.toPath());
1646 } catch (IOException e) {
1647 System.err.println("Failed to delete groups temp file “" + groupsFile + "”: " + e.getMessage());
1648 }
1649 }
1650 }
1651
1652 private void sendContacts() throws IOException, UntrustedIdentityException {
1653 File contactsFile = Util.createTempFile();
1654
1655 try {
1656 try (OutputStream fos = new FileOutputStream(contactsFile)) {
1657 DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos);
1658 for (ContactInfo record : contactStore.getContacts()) {
1659 VerifiedMessage verifiedMessage = null;
1660 ThreadInfo info = threadStore.getThread(record.number);
1661 if (getIdentities().containsKey(record.number)) {
1662 JsonIdentityKeyStore.Identity currentIdentity = null;
1663 for (JsonIdentityKeyStore.Identity id : getIdentities().get(record.number)) {
1664 if (currentIdentity == null || id.getDateAdded().after(currentIdentity.getDateAdded())) {
1665 currentIdentity = id;
1666 }
1667 }
1668 if (currentIdentity != null) {
1669 verifiedMessage = new VerifiedMessage(record.number, currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime());
1670 }
1671 }
1672
1673 byte[] profileKey = record.profileKey == null ? null : Base64.decode(record.profileKey);
1674 // TODO store list of blocked numbers
1675 boolean blocked = false;
1676 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
1677 createContactAvatarAttachment(record.number), Optional.fromNullable(record.color),
1678 Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), blocked, Optional.fromNullable(info != null ? info.messageExpirationTime : null)));
1679 }
1680 }
1681
1682 if (contactsFile.exists() && contactsFile.length() > 0) {
1683 try (FileInputStream contactsFileStream = new FileInputStream(contactsFile)) {
1684 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1685 .withStream(contactsFileStream)
1686 .withContentType("application/octet-stream")
1687 .withLength(contactsFile.length())
1688 .build();
1689
1690 sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, true)));
1691 }
1692 }
1693 } finally {
1694 try {
1695 Files.delete(contactsFile.toPath());
1696 } catch (IOException e) {
1697 System.err.println("Failed to delete contacts temp file “" + contactsFile + "”: " + e.getMessage());
1698 }
1699 }
1700 }
1701
1702 private void sendVerifiedMessage(String destination, IdentityKey identityKey, TrustLevel trustLevel) throws IOException, UntrustedIdentityException {
1703 VerifiedMessage verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState(), System.currentTimeMillis());
1704 sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage));
1705 }
1706
1707 public ContactInfo getContact(String number) {
1708 return contactStore.getContact(number);
1709 }
1710
1711 public GroupInfo getGroup(byte[] groupId) {
1712 return groupStore.getGroup(groupId);
1713 }
1714
1715 public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
1716 return signalProtocolStore.getIdentities();
1717 }
1718
1719 public List<JsonIdentityKeyStore.Identity> getIdentities(String number) {
1720 return signalProtocolStore.getIdentities(number);
1721 }
1722
1723 /**
1724 * Trust this the identity with this fingerprint
1725 *
1726 * @param name username of the identity
1727 * @param fingerprint Fingerprint
1728 */
1729 public boolean trustIdentityVerified(String name, byte[] fingerprint) {
1730 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1731 if (ids == null) {
1732 return false;
1733 }
1734 for (JsonIdentityKeyStore.Identity id : ids) {
1735 if (!Arrays.equals(id.getIdentityKey().serialize(), fingerprint)) {
1736 continue;
1737 }
1738
1739 signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
1740 try {
1741 sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
1742 } catch (IOException | UntrustedIdentityException e) {
1743 e.printStackTrace();
1744 }
1745 save();
1746 return true;
1747 }
1748 return false;
1749 }
1750
1751 /**
1752 * Trust this the identity with this safety number
1753 *
1754 * @param name username of the identity
1755 * @param safetyNumber Safety number
1756 */
1757 public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) {
1758 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1759 if (ids == null) {
1760 return false;
1761 }
1762 for (JsonIdentityKeyStore.Identity id : ids) {
1763 if (!safetyNumber.equals(computeSafetyNumber(name, id.getIdentityKey()))) {
1764 continue;
1765 }
1766
1767 signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
1768 try {
1769 sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
1770 } catch (IOException | UntrustedIdentityException e) {
1771 e.printStackTrace();
1772 }
1773 save();
1774 return true;
1775 }
1776 return false;
1777 }
1778
1779 /**
1780 * Trust all keys of this identity without verification
1781 *
1782 * @param name username of the identity
1783 */
1784 public boolean trustIdentityAllKeys(String name) {
1785 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1786 if (ids == null) {
1787 return false;
1788 }
1789 for (JsonIdentityKeyStore.Identity id : ids) {
1790 if (id.getTrustLevel() == TrustLevel.UNTRUSTED) {
1791 signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
1792 try {
1793 sendVerifiedMessage(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
1794 } catch (IOException | UntrustedIdentityException e) {
1795 e.printStackTrace();
1796 }
1797 }
1798 }
1799 save();
1800 return true;
1801 }
1802
1803 public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) {
1804 Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(username, getIdentity(), theirUsername, theirIdentityKey);
1805 return fingerprint.getDisplayableFingerprint().getDisplayText();
1806 }
1807 }