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