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