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