]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
Implement updateGroup command via dbus
[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.Base64;
39 import org.asamk.signal.util.Util;
40 import org.whispersystems.libsignal.*;
41 import org.whispersystems.libsignal.ecc.Curve;
42 import org.whispersystems.libsignal.ecc.ECKeyPair;
43 import org.whispersystems.libsignal.ecc.ECPublicKey;
44 import org.whispersystems.libsignal.fingerprint.Fingerprint;
45 import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
46 import org.whispersystems.libsignal.state.PreKeyRecord;
47 import org.whispersystems.libsignal.state.SignedPreKeyRecord;
48 import org.whispersystems.libsignal.util.KeyHelper;
49 import org.whispersystems.libsignal.util.Medium;
50 import org.whispersystems.libsignal.util.guava.Optional;
51 import org.whispersystems.signalservice.api.SignalServiceAccountManager;
52 import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
53 import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
54 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
55 import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
56 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
57 import org.whispersystems.signalservice.api.messages.*;
58 import org.whispersystems.signalservice.api.messages.multidevice.*;
59 import org.whispersystems.signalservice.api.push.ContactTokenDetails;
60 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
61 import org.whispersystems.signalservice.api.push.TrustStore;
62 import org.whispersystems.signalservice.api.push.exceptions.*;
63 import org.whispersystems.signalservice.api.util.InvalidNumberException;
64 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
65 import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
66 import org.whispersystems.signalservice.internal.push.SignalServiceUrl;
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(), false, false, 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 PreKeyRecord getOrGenerateLastResortPreKey() {
485 if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) {
486 try {
487 return signalProtocolStore.loadPreKey(Medium.MAX_VALUE);
488 } catch (InvalidKeyIdException e) {
489 signalProtocolStore.removePreKey(Medium.MAX_VALUE);
490 }
491 }
492
493 ECKeyPair keyPair = Curve.generateKeyPair();
494 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
495
496 signalProtocolStore.storePreKey(Medium.MAX_VALUE, record);
497 save();
498
499 return record;
500 }
501
502 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
503 try {
504 ECKeyPair keyPair = Curve.generateKeyPair();
505 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
506 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
507
508 signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record);
509 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
510 save();
511
512 return record;
513 } catch (InvalidKeyException e) {
514 throw new AssertionError(e);
515 }
516 }
517
518 public void verifyAccount(String verificationCode) throws IOException {
519 verificationCode = verificationCode.replace("-", "");
520 signalingKey = Util.getSecret(52);
521 accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, false, true);
522
523 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
524 registered = true;
525
526 refreshPreKeys();
527 save();
528 }
529
530 private void refreshPreKeys() throws IOException {
531 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
532 PreKeyRecord lastResortKey = getOrGenerateLastResortPreKey();
533 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair());
534
535 accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
536 }
537
538
539 private static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
540 List<SignalServiceAttachment> SignalServiceAttachments = null;
541 if (attachments != null) {
542 SignalServiceAttachments = new ArrayList<>(attachments.size());
543 for (String attachment : attachments) {
544 try {
545 SignalServiceAttachments.add(createAttachment(new File(attachment)));
546 } catch (IOException e) {
547 throw new AttachmentInvalidException(attachment, e);
548 }
549 }
550 }
551 return SignalServiceAttachments;
552 }
553
554 private static SignalServiceAttachmentStream createAttachment(File attachmentFile) throws IOException {
555 InputStream attachmentStream = new FileInputStream(attachmentFile);
556 final long attachmentSize = attachmentFile.length();
557 String mime = Files.probeContentType(attachmentFile.toPath());
558 if (mime == null) {
559 mime = "application/octet-stream";
560 }
561 return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, Optional.of(attachmentFile.getName()), null);
562 }
563
564 private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException {
565 File file = getGroupAvatarFile(groupId);
566 if (!file.exists()) {
567 return Optional.absent();
568 }
569
570 return Optional.of(createAttachment(file));
571 }
572
573 private Optional<SignalServiceAttachmentStream> createContactAvatarAttachment(String number) throws IOException {
574 File file = getContactAvatarFile(number);
575 if (!file.exists()) {
576 return Optional.absent();
577 }
578
579 return Optional.of(createAttachment(file));
580 }
581
582 private GroupInfo getGroupForSending(byte[] groupId) throws GroupNotFoundException, NotAGroupMemberException {
583 GroupInfo g = groupStore.getGroup(groupId);
584 if (g == null) {
585 throw new GroupNotFoundException(groupId);
586 }
587 for (String member : g.members) {
588 if (member.equals(this.username)) {
589 return g;
590 }
591 }
592 throw new NotAGroupMemberException(groupId, g.name);
593 }
594
595 public List<GroupInfo> getGroups() {
596 return groupStore.getGroups();
597 }
598
599 @Override
600 public void sendGroupMessage(String messageText, List<String> attachments,
601 byte[] groupId)
602 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
603 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
604 if (attachments != null) {
605 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
606 }
607 if (groupId != null) {
608 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
609 .withId(groupId)
610 .build();
611 messageBuilder.asGroupMessage(group);
612 }
613 ThreadInfo thread = threadStore.getThread(Base64.encodeBytes(groupId));
614 if (thread != null) {
615 messageBuilder.withExpiration(thread.messageExpirationTime);
616 }
617
618 final GroupInfo g = getGroupForSending(groupId);
619
620 // Don't send group message to ourself
621 final List<String> membersSend = new ArrayList<>(g.members);
622 membersSend.remove(this.username);
623 sendMessage(messageBuilder, membersSend);
624 }
625
626 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
627 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
628 .withId(groupId)
629 .build();
630
631 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
632 .asGroupMessage(group);
633
634 final GroupInfo g = getGroupForSending(groupId);
635 g.members.remove(this.username);
636 groupStore.updateGroup(g);
637
638 sendMessage(messageBuilder, g.members);
639 }
640
641 private static String join(CharSequence separator, Iterable<? extends CharSequence> list) {
642 StringBuilder buf = new StringBuilder();
643 for (CharSequence str : list) {
644 if (buf.length() > 0) {
645 buf.append(separator);
646 }
647 buf.append(str);
648 }
649
650 return buf.toString();
651 }
652
653 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
654 GroupInfo g;
655 if (groupId == null) {
656 // Create new group
657 g = new GroupInfo(Util.getSecretBytes(16));
658 g.members.add(username);
659 } else {
660 g = getGroupForSending(groupId);
661 }
662
663 if (name != null) {
664 g.name = name;
665 }
666
667 if (members != null) {
668 Set<String> newMembers = new HashSet<>();
669 for (String member : members) {
670 try {
671 member = canonicalizeNumber(member);
672 } catch (InvalidNumberException e) {
673 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
674 System.err.println("Aborting…");
675 System.exit(1);
676 }
677 if (g.members.contains(member)) {
678 continue;
679 }
680 newMembers.add(member);
681 g.members.add(member);
682 }
683 final List<ContactTokenDetails> contacts = accountManager.getContacts(newMembers);
684 if (contacts.size() != newMembers.size()) {
685 // Some of the new members are not registered on Signal
686 for (ContactTokenDetails contact : contacts) {
687 newMembers.remove(contact.getNumber());
688 }
689 System.err.println("Failed to add members " + join(", ", newMembers) + " to group: Not registered on Signal");
690 System.err.println("Aborting…");
691 System.exit(1);
692 }
693 }
694
695 if (avatarFile != null) {
696 createPrivateDirectories(avatarsPath);
697 File aFile = getGroupAvatarFile(g.groupId);
698 Files.copy(Paths.get(avatarFile), aFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
699 }
700
701 groupStore.updateGroup(g);
702
703 SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
704
705 // Don't send group message to ourself
706 final List<String> membersSend = new ArrayList<>(g.members);
707 membersSend.remove(this.username);
708 sendMessage(messageBuilder, membersSend);
709 return g.groupId;
710 }
711
712 private void sendUpdateGroupMessage(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
713 if (groupId == null) {
714 return;
715 }
716 GroupInfo g = getGroupForSending(groupId);
717
718 if (!g.members.contains(recipient)) {
719 return;
720 }
721
722 SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g);
723
724 // Send group message only to the recipient who requested it
725 final List<String> membersSend = new ArrayList<>();
726 membersSend.add(recipient);
727 sendMessage(messageBuilder, membersSend);
728 }
729
730 private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfo g) {
731 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
732 .withId(g.groupId)
733 .withName(g.name)
734 .withMembers(new ArrayList<>(g.members));
735
736 File aFile = getGroupAvatarFile(g.groupId);
737 if (aFile.exists()) {
738 try {
739 group.withAvatar(createAttachment(aFile));
740 } catch (IOException e) {
741 throw new AttachmentInvalidException(aFile.toString(), e);
742 }
743 }
744
745 return SignalServiceDataMessage.newBuilder()
746 .asGroupMessage(group.build());
747 }
748
749 private void sendGroupInfoRequest(byte[] groupId, String recipient) throws IOException, EncapsulatedExceptions {
750 if (groupId == null) {
751 return;
752 }
753
754 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO)
755 .withId(groupId);
756
757 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
758 .asGroupMessage(group.build());
759
760 // Send group info request message to the recipient who sent us a message with this groupId
761 final List<String> membersSend = new ArrayList<>();
762 membersSend.add(recipient);
763 sendMessage(messageBuilder, membersSend);
764 }
765
766 @Override
767 public void sendMessage(String message, List<String> attachments, String recipient)
768 throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
769 List<String> recipients = new ArrayList<>(1);
770 recipients.add(recipient);
771 sendMessage(message, attachments, recipients);
772 }
773
774 @Override
775 public void sendMessage(String messageText, List<String> attachments,
776 List<String> recipients)
777 throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
778 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
779 if (attachments != null) {
780 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
781 }
782 sendMessage(messageBuilder, recipients);
783 }
784
785 @Override
786 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
787 SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder()
788 .asEndSessionMessage();
789
790 sendMessage(messageBuilder, recipients);
791 }
792
793 @Override
794 public String getContactName(String number) {
795 ContactInfo contact = contactStore.getContact(number);
796 if (contact == null) {
797 return "";
798 } else {
799 return contact.name;
800 }
801 }
802
803 @Override
804 public void setContactName(String number, String name) {
805 ContactInfo contact = contactStore.getContact(number);
806 if (contact == null) {
807 contact = new ContactInfo();
808 contact.number = number;
809 System.out.println("Add contact " + number + " named " + name);
810 } else {
811 System.out.println("Updating contact " + number + " name " + contact.name + " -> " + name);
812 }
813 contact.name = name;
814 contactStore.updateContact(contact);
815 save();
816 }
817
818 @Override
819 public String getGroupName(byte[] groupId) {
820 GroupInfo group = getGroup(groupId);
821 if (group == null) {
822 return "";
823 } else {
824 return group.name;
825 }
826 }
827
828 @Override
829 public List<String> getGroupMembers(byte[] groupId) {
830 GroupInfo group = getGroup(groupId);
831 if (group == null) {
832 return new ArrayList<String>();
833 } else {
834 return new ArrayList<String>(group.members);
835 }
836 }
837
838 @Override
839 public byte[] updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
840 if (groupId.length == 0) {
841 groupId = null;
842 }
843 if (name.isEmpty()) {
844 name = null;
845 }
846 if (members.size() == 0) {
847 members = null;
848 }
849 if (avatar.isEmpty()) {
850 avatar = null;
851 }
852 return sendUpdateGroupMessage(groupId, name, members, avatar);
853 }
854
855 private void requestSyncGroups() throws IOException {
856 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
857 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
858 try {
859 sendSyncMessage(message);
860 } catch (UntrustedIdentityException e) {
861 e.printStackTrace();
862 }
863 }
864
865 private void requestSyncContacts() throws IOException {
866 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
867 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
868 try {
869 sendSyncMessage(message);
870 } catch (UntrustedIdentityException e) {
871 e.printStackTrace();
872 }
873 }
874
875 private void sendSyncMessage(SignalServiceSyncMessage message)
876 throws IOException, UntrustedIdentityException {
877 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password,
878 deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
879 try {
880 messageSender.sendMessage(message);
881 } catch (UntrustedIdentityException e) {
882 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
883 throw e;
884 }
885 }
886
887 private void sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
888 throws EncapsulatedExceptions, IOException {
889 Set<SignalServiceAddress> recipientsTS = getSignalServiceAddresses(recipients);
890 if (recipientsTS == null) return;
891
892 SignalServiceDataMessage message = null;
893 try {
894 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password,
895 deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
896
897 message = messageBuilder.build();
898 if (message.getGroupInfo().isPresent()) {
899 try {
900 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
901 } catch (EncapsulatedExceptions encapsulatedExceptions) {
902 for (UntrustedIdentityException e : encapsulatedExceptions.getUntrustedIdentityExceptions()) {
903 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
904 }
905 }
906 } else {
907 // Send to all individually, so sync messages are sent correctly
908 List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
909 List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
910 List<NetworkFailureException> networkExceptions = new LinkedList<>();
911 for (SignalServiceAddress address : recipientsTS) {
912 ThreadInfo thread = threadStore.getThread(address.getNumber());
913 if (thread != null) {
914 messageBuilder.withExpiration(thread.messageExpirationTime);
915 } else {
916 messageBuilder.withExpiration(0);
917 }
918 message = messageBuilder.build();
919 try {
920 messageSender.sendMessage(address, message);
921 } catch (UntrustedIdentityException e) {
922 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
923 untrustedIdentities.add(e);
924 } catch (UnregisteredUserException e) {
925 unregisteredUsers.add(e);
926 } catch (PushNetworkException e) {
927 networkExceptions.add(new NetworkFailureException(address.getNumber(), e));
928 }
929 }
930 if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
931 throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
932 }
933 }
934 } finally {
935 if (message != null && message.isEndSession()) {
936 for (SignalServiceAddress recipient : recipientsTS) {
937 handleEndSession(recipient.getNumber());
938 }
939 }
940 save();
941 }
942 }
943
944 private Set<SignalServiceAddress> getSignalServiceAddresses(Collection<String> recipients) {
945 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
946 for (String recipient : recipients) {
947 try {
948 recipientsTS.add(getPushAddress(recipient));
949 } catch (InvalidNumberException e) {
950 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
951 System.err.println("Aborting sending.");
952 save();
953 return null;
954 }
955 }
956 return recipientsTS;
957 }
958
959 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException {
960 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
961 try {
962 return cipher.decrypt(envelope);
963 } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
964 signalProtocolStore.saveIdentity(e.getName(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
965 throw e;
966 }
967 }
968
969 private void handleEndSession(String source) {
970 signalProtocolStore.deleteAllSessions(source);
971 }
972
973 public interface ReceiveMessageHandler {
974 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e);
975 }
976
977 private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination, boolean ignoreAttachments) {
978 String threadId;
979 if (message.getGroupInfo().isPresent()) {
980 SignalServiceGroup groupInfo = message.getGroupInfo().get();
981 threadId = Base64.encodeBytes(groupInfo.getGroupId());
982 GroupInfo group = groupStore.getGroup(groupInfo.getGroupId());
983 switch (groupInfo.getType()) {
984 case UPDATE:
985 if (group == null) {
986 group = new GroupInfo(groupInfo.getGroupId());
987 }
988
989 if (groupInfo.getAvatar().isPresent()) {
990 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
991 if (avatar.isPointer()) {
992 try {
993 retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
994 } catch (IOException | InvalidMessageException e) {
995 System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage());
996 }
997 }
998 }
999
1000 if (groupInfo.getName().isPresent()) {
1001 group.name = groupInfo.getName().get();
1002 }
1003
1004 if (groupInfo.getMembers().isPresent()) {
1005 group.members.addAll(groupInfo.getMembers().get());
1006 }
1007
1008 groupStore.updateGroup(group);
1009 break;
1010 case DELIVER:
1011 if (group == null) {
1012 try {
1013 sendGroupInfoRequest(groupInfo.getGroupId(), source);
1014 } catch (IOException | EncapsulatedExceptions e) {
1015 e.printStackTrace();
1016 }
1017 }
1018 break;
1019 case QUIT:
1020 if (group == null) {
1021 try {
1022 sendGroupInfoRequest(groupInfo.getGroupId(), source);
1023 } catch (IOException | EncapsulatedExceptions e) {
1024 e.printStackTrace();
1025 }
1026 } else {
1027 group.members.remove(source);
1028 groupStore.updateGroup(group);
1029 }
1030 break;
1031 case REQUEST_INFO:
1032 if (group != null) {
1033 try {
1034 sendUpdateGroupMessage(groupInfo.getGroupId(), source);
1035 } catch (IOException | EncapsulatedExceptions e) {
1036 e.printStackTrace();
1037 } catch (NotAGroupMemberException e) {
1038 // We have left this group, so don't send a group update message
1039 }
1040 }
1041 break;
1042 }
1043 } else {
1044 if (isSync) {
1045 threadId = destination;
1046 } else {
1047 threadId = source;
1048 }
1049 }
1050 if (message.isEndSession()) {
1051 handleEndSession(isSync ? destination : source);
1052 }
1053 if (message.isExpirationUpdate() || message.getBody().isPresent()) {
1054 ThreadInfo thread = threadStore.getThread(threadId);
1055 if (thread == null) {
1056 thread = new ThreadInfo();
1057 thread.id = threadId;
1058 }
1059 if (thread.messageExpirationTime != message.getExpiresInSeconds()) {
1060 thread.messageExpirationTime = message.getExpiresInSeconds();
1061 threadStore.updateThread(thread);
1062 }
1063 }
1064 if (message.getAttachments().isPresent() && !ignoreAttachments) {
1065 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
1066 if (attachment.isPointer()) {
1067 try {
1068 retrieveAttachment(attachment.asPointer());
1069 } catch (IOException | InvalidMessageException e) {
1070 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
1071 }
1072 }
1073 }
1074 }
1075 }
1076
1077 public void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
1078 final File cachePath = new File(getMessageCachePath());
1079 if (!cachePath.exists()) {
1080 return;
1081 }
1082 for (final File dir : cachePath.listFiles()) {
1083 if (!dir.isDirectory()) {
1084 continue;
1085 }
1086
1087 for (final File fileEntry : dir.listFiles()) {
1088 if (!fileEntry.isFile()) {
1089 continue;
1090 }
1091 SignalServiceEnvelope envelope;
1092 try {
1093 envelope = loadEnvelope(fileEntry);
1094 if (envelope == null) {
1095 continue;
1096 }
1097 } catch (IOException e) {
1098 e.printStackTrace();
1099 continue;
1100 }
1101 SignalServiceContent content = null;
1102 if (!envelope.isReceipt()) {
1103 try {
1104 content = decryptMessage(envelope);
1105 } catch (Exception e) {
1106 continue;
1107 }
1108 handleMessage(envelope, content, ignoreAttachments);
1109 }
1110 save();
1111 handler.handleMessage(envelope, content, null);
1112 try {
1113 Files.delete(fileEntry.toPath());
1114 } catch (IOException e) {
1115 System.out.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
1116 }
1117 }
1118 }
1119 }
1120
1121 public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException {
1122 retryFailedReceivedMessages(handler, ignoreAttachments);
1123 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1124
1125 try {
1126 if (messagePipe == null) {
1127 messagePipe = messageReceiver.createMessagePipe();
1128 }
1129
1130 while (true) {
1131 SignalServiceEnvelope envelope;
1132 SignalServiceContent content = null;
1133 Exception exception = null;
1134 final long now = new Date().getTime();
1135 try {
1136 envelope = messagePipe.read(timeout, unit, new SignalServiceMessagePipe.MessagePipeCallback() {
1137 @Override
1138 public void onMessage(SignalServiceEnvelope envelope) {
1139 // store message on disk, before acknowledging receipt to the server
1140 try {
1141 File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1142 storeEnvelope(envelope, cacheFile);
1143 } catch (IOException e) {
1144 System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
1145 }
1146 }
1147 });
1148 } catch (TimeoutException e) {
1149 if (returnOnTimeout)
1150 return;
1151 continue;
1152 } catch (InvalidVersionException e) {
1153 System.err.println("Ignoring error: " + e.getMessage());
1154 continue;
1155 }
1156 if (!envelope.isReceipt()) {
1157 try {
1158 content = decryptMessage(envelope);
1159 } catch (Exception e) {
1160 exception = e;
1161 }
1162 handleMessage(envelope, content, ignoreAttachments);
1163 }
1164 save();
1165 handler.handleMessage(envelope, content, exception);
1166 if (exception == null || !(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
1167 File cacheFile = null;
1168 try {
1169 cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1170 Files.delete(cacheFile.toPath());
1171 } catch (IOException e) {
1172 System.out.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
1173 }
1174 }
1175 }
1176 } finally {
1177 if (messagePipe != null) {
1178 messagePipe.shutdown();
1179 messagePipe = null;
1180 }
1181 }
1182 }
1183
1184 private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
1185 if (content != null) {
1186 if (content.getDataMessage().isPresent()) {
1187 SignalServiceDataMessage message = content.getDataMessage().get();
1188 handleSignalServiceDataMessage(message, false, envelope.getSource(), username, ignoreAttachments);
1189 }
1190 if (content.getSyncMessage().isPresent()) {
1191 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
1192 if (syncMessage.getSent().isPresent()) {
1193 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
1194 handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get(), ignoreAttachments);
1195 }
1196 if (syncMessage.getRequest().isPresent()) {
1197 RequestMessage rm = syncMessage.getRequest().get();
1198 if (rm.isContactsRequest()) {
1199 try {
1200 sendContacts();
1201 } catch (UntrustedIdentityException | IOException e) {
1202 e.printStackTrace();
1203 }
1204 }
1205 if (rm.isGroupsRequest()) {
1206 try {
1207 sendGroups();
1208 } catch (UntrustedIdentityException | IOException e) {
1209 e.printStackTrace();
1210 }
1211 }
1212 }
1213 if (syncMessage.getGroups().isPresent()) {
1214 File tmpFile = null;
1215 try {
1216 tmpFile = Util.createTempFile();
1217 DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile));
1218 DeviceGroup g;
1219 while ((g = s.read()) != null) {
1220 GroupInfo syncGroup = groupStore.getGroup(g.getId());
1221 if (syncGroup == null) {
1222 syncGroup = new GroupInfo(g.getId());
1223 }
1224 if (g.getName().isPresent()) {
1225 syncGroup.name = g.getName().get();
1226 }
1227 syncGroup.members.addAll(g.getMembers());
1228 syncGroup.active = g.isActive();
1229
1230 if (g.getAvatar().isPresent()) {
1231 retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
1232 }
1233 groupStore.updateGroup(syncGroup);
1234 }
1235 } catch (Exception e) {
1236 e.printStackTrace();
1237 } finally {
1238 if (tmpFile != null) {
1239 try {
1240 Files.delete(tmpFile.toPath());
1241 } catch (IOException e) {
1242 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1243 }
1244 }
1245 }
1246 if (syncMessage.getBlockedList().isPresent()) {
1247 // TODO store list of blocked numbers
1248 }
1249 }
1250 if (syncMessage.getContacts().isPresent()) {
1251 File tmpFile = null;
1252 try {
1253 tmpFile = Util.createTempFile();
1254 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer(), tmpFile));
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 } catch (Exception e) {
1275 e.printStackTrace();
1276 } finally {
1277 if (tmpFile != null) {
1278 try {
1279 Files.delete(tmpFile.toPath());
1280 } catch (IOException e) {
1281 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1282 }
1283 }
1284 }
1285 }
1286 }
1287 }
1288 }
1289
1290 private SignalServiceEnvelope loadEnvelope(File file) throws IOException {
1291 try (FileInputStream f = new FileInputStream(file)) {
1292 DataInputStream in = new DataInputStream(f);
1293 int version = in.readInt();
1294 if (version != 1) {
1295 return null;
1296 }
1297 int type = in.readInt();
1298 String source = in.readUTF();
1299 int sourceDevice = in.readInt();
1300 String relay = in.readUTF();
1301 long timestamp = in.readLong();
1302 byte[] content = null;
1303 int contentLen = in.readInt();
1304 if (contentLen > 0) {
1305 content = new byte[contentLen];
1306 in.readFully(content);
1307 }
1308 byte[] legacyMessage = null;
1309 int legacyMessageLen = in.readInt();
1310 if (legacyMessageLen > 0) {
1311 legacyMessage = new byte[legacyMessageLen];
1312 in.readFully(legacyMessage);
1313 }
1314 return new SignalServiceEnvelope(type, source, sourceDevice, relay, timestamp, legacyMessage, content);
1315 }
1316 }
1317
1318 private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
1319 try (FileOutputStream f = new FileOutputStream(file)) {
1320 try (DataOutputStream out = new DataOutputStream(f)) {
1321 out.writeInt(1); // version
1322 out.writeInt(envelope.getType());
1323 out.writeUTF(envelope.getSource());
1324 out.writeInt(envelope.getSourceDevice());
1325 out.writeUTF(envelope.getRelay());
1326 out.writeLong(envelope.getTimestamp());
1327 if (envelope.hasContent()) {
1328 out.writeInt(envelope.getContent().length);
1329 out.write(envelope.getContent());
1330 } else {
1331 out.writeInt(0);
1332 }
1333 if (envelope.hasLegacyMessage()) {
1334 out.writeInt(envelope.getLegacyMessage().length);
1335 out.write(envelope.getLegacyMessage());
1336 } else {
1337 out.writeInt(0);
1338 }
1339 }
1340 }
1341 }
1342
1343 public File getContactAvatarFile(String number) {
1344 return new File(avatarsPath, "contact-" + number);
1345 }
1346
1347 private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
1348 createPrivateDirectories(avatarsPath);
1349 if (attachment.isPointer()) {
1350 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1351 return retrieveAttachment(pointer, getContactAvatarFile(number), false);
1352 } else {
1353 SignalServiceAttachmentStream stream = attachment.asStream();
1354 return retrieveAttachment(stream, getContactAvatarFile(number));
1355 }
1356 }
1357
1358 public File getGroupAvatarFile(byte[] groupId) {
1359 return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
1360 }
1361
1362 private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
1363 createPrivateDirectories(avatarsPath);
1364 if (attachment.isPointer()) {
1365 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1366 return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
1367 } else {
1368 SignalServiceAttachmentStream stream = attachment.asStream();
1369 return retrieveAttachment(stream, getGroupAvatarFile(groupId));
1370 }
1371 }
1372
1373 public File getAttachmentFile(long attachmentId) {
1374 return new File(attachmentsPath, attachmentId + "");
1375 }
1376
1377 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1378 createPrivateDirectories(attachmentsPath);
1379 return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
1380 }
1381
1382 private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
1383 InputStream input = stream.getInputStream();
1384
1385 try (OutputStream output = new FileOutputStream(outputFile)) {
1386 byte[] buffer = new byte[4096];
1387 int read;
1388
1389 while ((read = input.read(buffer)) != -1) {
1390 output.write(buffer, 0, read);
1391 }
1392 } catch (FileNotFoundException e) {
1393 e.printStackTrace();
1394 return null;
1395 }
1396 return outputFile;
1397 }
1398
1399 private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
1400 if (storePreview && pointer.getPreview().isPresent()) {
1401 File previewFile = new File(outputFile + ".preview");
1402 try (OutputStream output = new FileOutputStream(previewFile)) {
1403 byte[] preview = pointer.getPreview().get();
1404 output.write(preview, 0, preview.length);
1405 } catch (FileNotFoundException e) {
1406 e.printStackTrace();
1407 return null;
1408 }
1409 }
1410
1411 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1412
1413 File tmpFile = Util.createTempFile();
1414 try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE)) {
1415 try (OutputStream output = new FileOutputStream(outputFile)) {
1416 byte[] buffer = new byte[4096];
1417 int read;
1418
1419 while ((read = input.read(buffer)) != -1) {
1420 output.write(buffer, 0, read);
1421 }
1422 } catch (FileNotFoundException e) {
1423 e.printStackTrace();
1424 return null;
1425 }
1426 } finally {
1427 try {
1428 Files.delete(tmpFile.toPath());
1429 } catch (IOException e) {
1430 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1431 }
1432 }
1433 return outputFile;
1434 }
1435
1436 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException {
1437 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1438 return messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE);
1439 }
1440
1441 private String canonicalizeNumber(String number) throws InvalidNumberException {
1442 String localNumber = username;
1443 return PhoneNumberFormatter.formatNumber(number, localNumber);
1444 }
1445
1446 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
1447 String e164number = canonicalizeNumber(number);
1448 return new SignalServiceAddress(e164number);
1449 }
1450
1451 @Override
1452 public boolean isRemote() {
1453 return false;
1454 }
1455
1456 private void sendGroups() throws IOException, UntrustedIdentityException {
1457 File groupsFile = Util.createTempFile();
1458
1459 try {
1460 try (OutputStream fos = new FileOutputStream(groupsFile)) {
1461 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
1462 for (GroupInfo record : groupStore.getGroups()) {
1463 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
1464 new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
1465 record.active));
1466 }
1467 }
1468
1469 if (groupsFile.exists() && groupsFile.length() > 0) {
1470 try (FileInputStream groupsFileStream = new FileInputStream(groupsFile)) {
1471 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1472 .withStream(groupsFileStream)
1473 .withContentType("application/octet-stream")
1474 .withLength(groupsFile.length())
1475 .build();
1476
1477 sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
1478 }
1479 }
1480 } finally {
1481 try {
1482 Files.delete(groupsFile.toPath());
1483 } catch (IOException e) {
1484 System.out.println("Failed to delete temp file “" + groupsFile + "”: " + e.getMessage());
1485 }
1486 }
1487 }
1488
1489 private void sendContacts() throws IOException, UntrustedIdentityException {
1490 File contactsFile = Util.createTempFile();
1491
1492 try {
1493 try (OutputStream fos = new FileOutputStream(contactsFile)) {
1494 DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos);
1495 for (ContactInfo record : contactStore.getContacts()) {
1496 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
1497 createContactAvatarAttachment(record.number), Optional.fromNullable(record.color)));
1498 }
1499 }
1500
1501 if (contactsFile.exists() && contactsFile.length() > 0) {
1502 try (FileInputStream contactsFileStream = new FileInputStream(contactsFile)) {
1503 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1504 .withStream(contactsFileStream)
1505 .withContentType("application/octet-stream")
1506 .withLength(contactsFile.length())
1507 .build();
1508
1509 sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
1510 }
1511 }
1512 } finally {
1513 try {
1514 Files.delete(contactsFile.toPath());
1515 } catch (IOException e) {
1516 System.out.println("Failed to delete temp file “" + contactsFile + "”: " + e.getMessage());
1517 }
1518 }
1519 }
1520
1521 public ContactInfo getContact(String number) {
1522 return contactStore.getContact(number);
1523 }
1524
1525 public GroupInfo getGroup(byte[] groupId) {
1526 return groupStore.getGroup(groupId);
1527 }
1528
1529 public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
1530 return signalProtocolStore.getIdentities();
1531 }
1532
1533 public List<JsonIdentityKeyStore.Identity> getIdentities(String number) {
1534 return signalProtocolStore.getIdentities(number);
1535 }
1536
1537 /**
1538 * Trust this the identity with this fingerprint
1539 *
1540 * @param name username of the identity
1541 * @param fingerprint Fingerprint
1542 */
1543 public boolean trustIdentityVerified(String name, byte[] fingerprint) {
1544 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1545 if (ids == null) {
1546 return false;
1547 }
1548 for (JsonIdentityKeyStore.Identity id : ids) {
1549 if (!Arrays.equals(id.getIdentityKey().serialize(), fingerprint)) {
1550 continue;
1551 }
1552
1553 signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
1554 save();
1555 return true;
1556 }
1557 return false;
1558 }
1559
1560 /**
1561 * Trust this the identity with this safety number
1562 *
1563 * @param name username of the identity
1564 * @param safetyNumber Safety number
1565 */
1566 public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) {
1567 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1568 if (ids == null) {
1569 return false;
1570 }
1571 for (JsonIdentityKeyStore.Identity id : ids) {
1572 if (!safetyNumber.equals(computeSafetyNumber(name, id.getIdentityKey()))) {
1573 continue;
1574 }
1575
1576 signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
1577 save();
1578 return true;
1579 }
1580 return false;
1581 }
1582
1583 /**
1584 * Trust all keys of this identity without verification
1585 *
1586 * @param name username of the identity
1587 */
1588 public boolean trustIdentityAllKeys(String name) {
1589 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1590 if (ids == null) {
1591 return false;
1592 }
1593 for (JsonIdentityKeyStore.Identity id : ids) {
1594 if (id.getTrustLevel() == TrustLevel.UNTRUSTED) {
1595 signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
1596 }
1597 }
1598 save();
1599 return true;
1600 }
1601
1602 public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) {
1603 Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(username, getIdentity(), theirUsername, theirIdentityKey);
1604 return fingerprint.getDisplayableFingerprint().getDisplayText();
1605 }
1606 }