]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
575bba222b243862a5102bb4f97c9ece96690fd2
[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 void updateGroup(byte[] groupId, String name, List<String> members, String avatar) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
840 if (name.isEmpty()) {
841 name = null;
842 }
843 if (members.size() == 0) {
844 members = null;
845 }
846 if (avatar.isEmpty()) {
847 avatar = null;
848 }
849 sendUpdateGroupMessage(groupId, name, members, avatar);
850 }
851
852 private void requestSyncGroups() throws IOException {
853 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS).build();
854 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
855 try {
856 sendSyncMessage(message);
857 } catch (UntrustedIdentityException e) {
858 e.printStackTrace();
859 }
860 }
861
862 private void requestSyncContacts() throws IOException {
863 SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder().setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS).build();
864 SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r));
865 try {
866 sendSyncMessage(message);
867 } catch (UntrustedIdentityException e) {
868 e.printStackTrace();
869 }
870 }
871
872 private void sendSyncMessage(SignalServiceSyncMessage message)
873 throws IOException, UntrustedIdentityException {
874 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password,
875 deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
876 try {
877 messageSender.sendMessage(message);
878 } catch (UntrustedIdentityException e) {
879 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
880 throw e;
881 }
882 }
883
884 private void sendMessage(SignalServiceDataMessage.Builder messageBuilder, Collection<String> recipients)
885 throws EncapsulatedExceptions, IOException {
886 Set<SignalServiceAddress> recipientsTS = getSignalServiceAddresses(recipients);
887 if (recipientsTS == null) return;
888
889 SignalServiceDataMessage message = null;
890 try {
891 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(serviceUrls, username, password,
892 deviceId, signalProtocolStore, USER_AGENT, Optional.fromNullable(messagePipe), Optional.<SignalServiceMessageSender.EventListener>absent());
893
894 message = messageBuilder.build();
895 if (message.getGroupInfo().isPresent()) {
896 try {
897 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
898 } catch (EncapsulatedExceptions encapsulatedExceptions) {
899 for (UntrustedIdentityException e : encapsulatedExceptions.getUntrustedIdentityExceptions()) {
900 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
901 }
902 }
903 } else {
904 // Send to all individually, so sync messages are sent correctly
905 List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
906 List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
907 List<NetworkFailureException> networkExceptions = new LinkedList<>();
908 for (SignalServiceAddress address : recipientsTS) {
909 ThreadInfo thread = threadStore.getThread(address.getNumber());
910 if (thread != null) {
911 messageBuilder.withExpiration(thread.messageExpirationTime);
912 } else {
913 messageBuilder.withExpiration(0);
914 }
915 message = messageBuilder.build();
916 try {
917 messageSender.sendMessage(address, message);
918 } catch (UntrustedIdentityException e) {
919 signalProtocolStore.saveIdentity(e.getE164Number(), e.getIdentityKey(), TrustLevel.UNTRUSTED);
920 untrustedIdentities.add(e);
921 } catch (UnregisteredUserException e) {
922 unregisteredUsers.add(e);
923 } catch (PushNetworkException e) {
924 networkExceptions.add(new NetworkFailureException(address.getNumber(), e));
925 }
926 }
927 if (!untrustedIdentities.isEmpty() || !unregisteredUsers.isEmpty() || !networkExceptions.isEmpty()) {
928 throw new EncapsulatedExceptions(untrustedIdentities, unregisteredUsers, networkExceptions);
929 }
930 }
931 } finally {
932 if (message != null && message.isEndSession()) {
933 for (SignalServiceAddress recipient : recipientsTS) {
934 handleEndSession(recipient.getNumber());
935 }
936 }
937 save();
938 }
939 }
940
941 private Set<SignalServiceAddress> getSignalServiceAddresses(Collection<String> recipients) {
942 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
943 for (String recipient : recipients) {
944 try {
945 recipientsTS.add(getPushAddress(recipient));
946 } catch (InvalidNumberException e) {
947 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
948 System.err.println("Aborting sending.");
949 save();
950 return null;
951 }
952 }
953 return recipientsTS;
954 }
955
956 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws NoSessionException, LegacyMessageException, InvalidVersionException, InvalidMessageException, DuplicateMessageException, InvalidKeyException, InvalidKeyIdException, org.whispersystems.libsignal.UntrustedIdentityException {
957 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
958 try {
959 return cipher.decrypt(envelope);
960 } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
961 signalProtocolStore.saveIdentity(e.getName(), e.getUntrustedIdentity(), TrustLevel.UNTRUSTED);
962 throw e;
963 }
964 }
965
966 private void handleEndSession(String source) {
967 signalProtocolStore.deleteAllSessions(source);
968 }
969
970 public interface ReceiveMessageHandler {
971 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e);
972 }
973
974 private void handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination, boolean ignoreAttachments) {
975 String threadId;
976 if (message.getGroupInfo().isPresent()) {
977 SignalServiceGroup groupInfo = message.getGroupInfo().get();
978 threadId = Base64.encodeBytes(groupInfo.getGroupId());
979 GroupInfo group = groupStore.getGroup(groupInfo.getGroupId());
980 switch (groupInfo.getType()) {
981 case UPDATE:
982 if (group == null) {
983 group = new GroupInfo(groupInfo.getGroupId());
984 }
985
986 if (groupInfo.getAvatar().isPresent()) {
987 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
988 if (avatar.isPointer()) {
989 try {
990 retrieveGroupAvatarAttachment(avatar.asPointer(), group.groupId);
991 } catch (IOException | InvalidMessageException e) {
992 System.err.println("Failed to retrieve group avatar (" + avatar.asPointer().getId() + "): " + e.getMessage());
993 }
994 }
995 }
996
997 if (groupInfo.getName().isPresent()) {
998 group.name = groupInfo.getName().get();
999 }
1000
1001 if (groupInfo.getMembers().isPresent()) {
1002 group.members.addAll(groupInfo.getMembers().get());
1003 }
1004
1005 groupStore.updateGroup(group);
1006 break;
1007 case DELIVER:
1008 if (group == null) {
1009 try {
1010 sendGroupInfoRequest(groupInfo.getGroupId(), source);
1011 } catch (IOException | EncapsulatedExceptions e) {
1012 e.printStackTrace();
1013 }
1014 }
1015 break;
1016 case QUIT:
1017 if (group == null) {
1018 try {
1019 sendGroupInfoRequest(groupInfo.getGroupId(), source);
1020 } catch (IOException | EncapsulatedExceptions e) {
1021 e.printStackTrace();
1022 }
1023 } else {
1024 group.members.remove(source);
1025 groupStore.updateGroup(group);
1026 }
1027 break;
1028 case REQUEST_INFO:
1029 if (group != null) {
1030 try {
1031 sendUpdateGroupMessage(groupInfo.getGroupId(), source);
1032 } catch (IOException | EncapsulatedExceptions e) {
1033 e.printStackTrace();
1034 } catch (NotAGroupMemberException e) {
1035 // We have left this group, so don't send a group update message
1036 }
1037 }
1038 break;
1039 }
1040 } else {
1041 if (isSync) {
1042 threadId = destination;
1043 } else {
1044 threadId = source;
1045 }
1046 }
1047 if (message.isEndSession()) {
1048 handleEndSession(isSync ? destination : source);
1049 }
1050 if (message.isExpirationUpdate() || message.getBody().isPresent()) {
1051 ThreadInfo thread = threadStore.getThread(threadId);
1052 if (thread == null) {
1053 thread = new ThreadInfo();
1054 thread.id = threadId;
1055 }
1056 if (thread.messageExpirationTime != message.getExpiresInSeconds()) {
1057 thread.messageExpirationTime = message.getExpiresInSeconds();
1058 threadStore.updateThread(thread);
1059 }
1060 }
1061 if (message.getAttachments().isPresent() && !ignoreAttachments) {
1062 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
1063 if (attachment.isPointer()) {
1064 try {
1065 retrieveAttachment(attachment.asPointer());
1066 } catch (IOException | InvalidMessageException e) {
1067 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
1068 }
1069 }
1070 }
1071 }
1072 }
1073
1074 public void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
1075 final File cachePath = new File(getMessageCachePath());
1076 if (!cachePath.exists()) {
1077 return;
1078 }
1079 for (final File dir : cachePath.listFiles()) {
1080 if (!dir.isDirectory()) {
1081 continue;
1082 }
1083
1084 for (final File fileEntry : dir.listFiles()) {
1085 if (!fileEntry.isFile()) {
1086 continue;
1087 }
1088 SignalServiceEnvelope envelope;
1089 try {
1090 envelope = loadEnvelope(fileEntry);
1091 if (envelope == null) {
1092 continue;
1093 }
1094 } catch (IOException e) {
1095 e.printStackTrace();
1096 continue;
1097 }
1098 SignalServiceContent content = null;
1099 if (!envelope.isReceipt()) {
1100 try {
1101 content = decryptMessage(envelope);
1102 } catch (Exception e) {
1103 continue;
1104 }
1105 handleMessage(envelope, content, ignoreAttachments);
1106 }
1107 save();
1108 handler.handleMessage(envelope, content, null);
1109 try {
1110 Files.delete(fileEntry.toPath());
1111 } catch (IOException e) {
1112 System.out.println("Failed to delete cached message file “" + fileEntry + "”: " + e.getMessage());
1113 }
1114 }
1115 }
1116 }
1117
1118 public void receiveMessages(long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler) throws IOException {
1119 retryFailedReceivedMessages(handler, ignoreAttachments);
1120 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1121
1122 try {
1123 if (messagePipe == null) {
1124 messagePipe = messageReceiver.createMessagePipe();
1125 }
1126
1127 while (true) {
1128 SignalServiceEnvelope envelope;
1129 SignalServiceContent content = null;
1130 Exception exception = null;
1131 final long now = new Date().getTime();
1132 try {
1133 envelope = messagePipe.read(timeout, unit, new SignalServiceMessagePipe.MessagePipeCallback() {
1134 @Override
1135 public void onMessage(SignalServiceEnvelope envelope) {
1136 // store message on disk, before acknowledging receipt to the server
1137 try {
1138 File cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1139 storeEnvelope(envelope, cacheFile);
1140 } catch (IOException e) {
1141 System.err.println("Failed to store encrypted message in disk cache, ignoring: " + e.getMessage());
1142 }
1143 }
1144 });
1145 } catch (TimeoutException e) {
1146 if (returnOnTimeout)
1147 return;
1148 continue;
1149 } catch (InvalidVersionException e) {
1150 System.err.println("Ignoring error: " + e.getMessage());
1151 continue;
1152 }
1153 if (!envelope.isReceipt()) {
1154 try {
1155 content = decryptMessage(envelope);
1156 } catch (Exception e) {
1157 exception = e;
1158 }
1159 handleMessage(envelope, content, ignoreAttachments);
1160 }
1161 save();
1162 handler.handleMessage(envelope, content, exception);
1163 if (exception == null || !(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
1164 File cacheFile = null;
1165 try {
1166 cacheFile = getMessageCacheFile(envelope.getSource(), now, envelope.getTimestamp());
1167 Files.delete(cacheFile.toPath());
1168 } catch (IOException e) {
1169 System.out.println("Failed to delete cached message file “" + cacheFile + "”: " + e.getMessage());
1170 }
1171 }
1172 }
1173 } finally {
1174 if (messagePipe != null) {
1175 messagePipe.shutdown();
1176 messagePipe = null;
1177 }
1178 }
1179 }
1180
1181 private void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments) {
1182 if (content != null) {
1183 if (content.getDataMessage().isPresent()) {
1184 SignalServiceDataMessage message = content.getDataMessage().get();
1185 handleSignalServiceDataMessage(message, false, envelope.getSource(), username, ignoreAttachments);
1186 }
1187 if (content.getSyncMessage().isPresent()) {
1188 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
1189 if (syncMessage.getSent().isPresent()) {
1190 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
1191 handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get(), ignoreAttachments);
1192 }
1193 if (syncMessage.getRequest().isPresent()) {
1194 RequestMessage rm = syncMessage.getRequest().get();
1195 if (rm.isContactsRequest()) {
1196 try {
1197 sendContacts();
1198 } catch (UntrustedIdentityException | IOException e) {
1199 e.printStackTrace();
1200 }
1201 }
1202 if (rm.isGroupsRequest()) {
1203 try {
1204 sendGroups();
1205 } catch (UntrustedIdentityException | IOException e) {
1206 e.printStackTrace();
1207 }
1208 }
1209 }
1210 if (syncMessage.getGroups().isPresent()) {
1211 File tmpFile = null;
1212 try {
1213 tmpFile = Util.createTempFile();
1214 DeviceGroupsInputStream s = new DeviceGroupsInputStream(retrieveAttachmentAsStream(syncMessage.getGroups().get().asPointer(), tmpFile));
1215 DeviceGroup g;
1216 while ((g = s.read()) != null) {
1217 GroupInfo syncGroup = groupStore.getGroup(g.getId());
1218 if (syncGroup == null) {
1219 syncGroup = new GroupInfo(g.getId());
1220 }
1221 if (g.getName().isPresent()) {
1222 syncGroup.name = g.getName().get();
1223 }
1224 syncGroup.members.addAll(g.getMembers());
1225 syncGroup.active = g.isActive();
1226
1227 if (g.getAvatar().isPresent()) {
1228 retrieveGroupAvatarAttachment(g.getAvatar().get(), syncGroup.groupId);
1229 }
1230 groupStore.updateGroup(syncGroup);
1231 }
1232 } catch (Exception e) {
1233 e.printStackTrace();
1234 } finally {
1235 if (tmpFile != null) {
1236 try {
1237 Files.delete(tmpFile.toPath());
1238 } catch (IOException e) {
1239 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1240 }
1241 }
1242 }
1243 if (syncMessage.getBlockedList().isPresent()) {
1244 // TODO store list of blocked numbers
1245 }
1246 }
1247 if (syncMessage.getContacts().isPresent()) {
1248 File tmpFile = null;
1249 try {
1250 tmpFile = Util.createTempFile();
1251 DeviceContactsInputStream s = new DeviceContactsInputStream(retrieveAttachmentAsStream(syncMessage.getContacts().get().asPointer(), tmpFile));
1252 DeviceContact c;
1253 while ((c = s.read()) != null) {
1254 ContactInfo contact = contactStore.getContact(c.getNumber());
1255 if (contact == null) {
1256 contact = new ContactInfo();
1257 contact.number = c.getNumber();
1258 }
1259 if (c.getName().isPresent()) {
1260 contact.name = c.getName().get();
1261 }
1262 if (c.getColor().isPresent()) {
1263 contact.color = c.getColor().get();
1264 }
1265 contactStore.updateContact(contact);
1266
1267 if (c.getAvatar().isPresent()) {
1268 retrieveContactAvatarAttachment(c.getAvatar().get(), contact.number);
1269 }
1270 }
1271 } catch (Exception e) {
1272 e.printStackTrace();
1273 } finally {
1274 if (tmpFile != null) {
1275 try {
1276 Files.delete(tmpFile.toPath());
1277 } catch (IOException e) {
1278 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1279 }
1280 }
1281 }
1282 }
1283 }
1284 }
1285 }
1286
1287 private SignalServiceEnvelope loadEnvelope(File file) throws IOException {
1288 try (FileInputStream f = new FileInputStream(file)) {
1289 DataInputStream in = new DataInputStream(f);
1290 int version = in.readInt();
1291 if (version != 1) {
1292 return null;
1293 }
1294 int type = in.readInt();
1295 String source = in.readUTF();
1296 int sourceDevice = in.readInt();
1297 String relay = in.readUTF();
1298 long timestamp = in.readLong();
1299 byte[] content = null;
1300 int contentLen = in.readInt();
1301 if (contentLen > 0) {
1302 content = new byte[contentLen];
1303 in.readFully(content);
1304 }
1305 byte[] legacyMessage = null;
1306 int legacyMessageLen = in.readInt();
1307 if (legacyMessageLen > 0) {
1308 legacyMessage = new byte[legacyMessageLen];
1309 in.readFully(legacyMessage);
1310 }
1311 return new SignalServiceEnvelope(type, source, sourceDevice, relay, timestamp, legacyMessage, content);
1312 }
1313 }
1314
1315 private void storeEnvelope(SignalServiceEnvelope envelope, File file) throws IOException {
1316 try (FileOutputStream f = new FileOutputStream(file)) {
1317 try (DataOutputStream out = new DataOutputStream(f)) {
1318 out.writeInt(1); // version
1319 out.writeInt(envelope.getType());
1320 out.writeUTF(envelope.getSource());
1321 out.writeInt(envelope.getSourceDevice());
1322 out.writeUTF(envelope.getRelay());
1323 out.writeLong(envelope.getTimestamp());
1324 if (envelope.hasContent()) {
1325 out.writeInt(envelope.getContent().length);
1326 out.write(envelope.getContent());
1327 } else {
1328 out.writeInt(0);
1329 }
1330 if (envelope.hasLegacyMessage()) {
1331 out.writeInt(envelope.getLegacyMessage().length);
1332 out.write(envelope.getLegacyMessage());
1333 } else {
1334 out.writeInt(0);
1335 }
1336 }
1337 }
1338 }
1339
1340 public File getContactAvatarFile(String number) {
1341 return new File(avatarsPath, "contact-" + number);
1342 }
1343
1344 private File retrieveContactAvatarAttachment(SignalServiceAttachment attachment, String number) throws IOException, InvalidMessageException {
1345 createPrivateDirectories(avatarsPath);
1346 if (attachment.isPointer()) {
1347 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1348 return retrieveAttachment(pointer, getContactAvatarFile(number), false);
1349 } else {
1350 SignalServiceAttachmentStream stream = attachment.asStream();
1351 return retrieveAttachment(stream, getContactAvatarFile(number));
1352 }
1353 }
1354
1355 public File getGroupAvatarFile(byte[] groupId) {
1356 return new File(avatarsPath, "group-" + Base64.encodeBytes(groupId).replace("/", "_"));
1357 }
1358
1359 private File retrieveGroupAvatarAttachment(SignalServiceAttachment attachment, byte[] groupId) throws IOException, InvalidMessageException {
1360 createPrivateDirectories(avatarsPath);
1361 if (attachment.isPointer()) {
1362 SignalServiceAttachmentPointer pointer = attachment.asPointer();
1363 return retrieveAttachment(pointer, getGroupAvatarFile(groupId), false);
1364 } else {
1365 SignalServiceAttachmentStream stream = attachment.asStream();
1366 return retrieveAttachment(stream, getGroupAvatarFile(groupId));
1367 }
1368 }
1369
1370 public File getAttachmentFile(long attachmentId) {
1371 return new File(attachmentsPath, attachmentId + "");
1372 }
1373
1374 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
1375 createPrivateDirectories(attachmentsPath);
1376 return retrieveAttachment(pointer, getAttachmentFile(pointer.getId()), true);
1377 }
1378
1379 private File retrieveAttachment(SignalServiceAttachmentStream stream, File outputFile) throws IOException, InvalidMessageException {
1380 InputStream input = stream.getInputStream();
1381
1382 try (OutputStream output = new FileOutputStream(outputFile)) {
1383 byte[] buffer = new byte[4096];
1384 int read;
1385
1386 while ((read = input.read(buffer)) != -1) {
1387 output.write(buffer, 0, read);
1388 }
1389 } catch (FileNotFoundException e) {
1390 e.printStackTrace();
1391 return null;
1392 }
1393 return outputFile;
1394 }
1395
1396 private File retrieveAttachment(SignalServiceAttachmentPointer pointer, File outputFile, boolean storePreview) throws IOException, InvalidMessageException {
1397 if (storePreview && pointer.getPreview().isPresent()) {
1398 File previewFile = new File(outputFile + ".preview");
1399 try (OutputStream output = new FileOutputStream(previewFile)) {
1400 byte[] preview = pointer.getPreview().get();
1401 output.write(preview, 0, preview.length);
1402 } catch (FileNotFoundException e) {
1403 e.printStackTrace();
1404 return null;
1405 }
1406 }
1407
1408 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1409
1410 File tmpFile = Util.createTempFile();
1411 try (InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE)) {
1412 try (OutputStream output = new FileOutputStream(outputFile)) {
1413 byte[] buffer = new byte[4096];
1414 int read;
1415
1416 while ((read = input.read(buffer)) != -1) {
1417 output.write(buffer, 0, read);
1418 }
1419 } catch (FileNotFoundException e) {
1420 e.printStackTrace();
1421 return null;
1422 }
1423 } finally {
1424 try {
1425 Files.delete(tmpFile.toPath());
1426 } catch (IOException e) {
1427 System.out.println("Failed to delete temp file “" + tmpFile + "”: " + e.getMessage());
1428 }
1429 }
1430 return outputFile;
1431 }
1432
1433 private InputStream retrieveAttachmentAsStream(SignalServiceAttachmentPointer pointer, File tmpFile) throws IOException, InvalidMessageException {
1434 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(serviceUrls, username, password, deviceId, signalingKey, USER_AGENT);
1435 return messageReceiver.retrieveAttachment(pointer, tmpFile, MAX_ATTACHMENT_SIZE);
1436 }
1437
1438 private String canonicalizeNumber(String number) throws InvalidNumberException {
1439 String localNumber = username;
1440 return PhoneNumberFormatter.formatNumber(number, localNumber);
1441 }
1442
1443 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
1444 String e164number = canonicalizeNumber(number);
1445 return new SignalServiceAddress(e164number);
1446 }
1447
1448 @Override
1449 public boolean isRemote() {
1450 return false;
1451 }
1452
1453 private void sendGroups() throws IOException, UntrustedIdentityException {
1454 File groupsFile = Util.createTempFile();
1455
1456 try {
1457 try (OutputStream fos = new FileOutputStream(groupsFile)) {
1458 DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos);
1459 for (GroupInfo record : groupStore.getGroups()) {
1460 out.write(new DeviceGroup(record.groupId, Optional.fromNullable(record.name),
1461 new ArrayList<>(record.members), createGroupAvatarAttachment(record.groupId),
1462 record.active));
1463 }
1464 }
1465
1466 if (groupsFile.exists() && groupsFile.length() > 0) {
1467 try (FileInputStream groupsFileStream = new FileInputStream(groupsFile)) {
1468 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1469 .withStream(groupsFileStream)
1470 .withContentType("application/octet-stream")
1471 .withLength(groupsFile.length())
1472 .build();
1473
1474 sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream));
1475 }
1476 }
1477 } finally {
1478 try {
1479 Files.delete(groupsFile.toPath());
1480 } catch (IOException e) {
1481 System.out.println("Failed to delete temp file “" + groupsFile + "”: " + e.getMessage());
1482 }
1483 }
1484 }
1485
1486 private void sendContacts() throws IOException, UntrustedIdentityException {
1487 File contactsFile = Util.createTempFile();
1488
1489 try {
1490 try (OutputStream fos = new FileOutputStream(contactsFile)) {
1491 DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos);
1492 for (ContactInfo record : contactStore.getContacts()) {
1493 out.write(new DeviceContact(record.number, Optional.fromNullable(record.name),
1494 createContactAvatarAttachment(record.number), Optional.fromNullable(record.color)));
1495 }
1496 }
1497
1498 if (contactsFile.exists() && contactsFile.length() > 0) {
1499 try (FileInputStream contactsFileStream = new FileInputStream(contactsFile)) {
1500 SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder()
1501 .withStream(contactsFileStream)
1502 .withContentType("application/octet-stream")
1503 .withLength(contactsFile.length())
1504 .build();
1505
1506 sendSyncMessage(SignalServiceSyncMessage.forContacts(attachmentStream));
1507 }
1508 }
1509 } finally {
1510 try {
1511 Files.delete(contactsFile.toPath());
1512 } catch (IOException e) {
1513 System.out.println("Failed to delete temp file “" + contactsFile + "”: " + e.getMessage());
1514 }
1515 }
1516 }
1517
1518 public ContactInfo getContact(String number) {
1519 return contactStore.getContact(number);
1520 }
1521
1522 public GroupInfo getGroup(byte[] groupId) {
1523 return groupStore.getGroup(groupId);
1524 }
1525
1526 public Map<String, List<JsonIdentityKeyStore.Identity>> getIdentities() {
1527 return signalProtocolStore.getIdentities();
1528 }
1529
1530 public List<JsonIdentityKeyStore.Identity> getIdentities(String number) {
1531 return signalProtocolStore.getIdentities(number);
1532 }
1533
1534 /**
1535 * Trust this the identity with this fingerprint
1536 *
1537 * @param name username of the identity
1538 * @param fingerprint Fingerprint
1539 */
1540 public boolean trustIdentityVerified(String name, byte[] fingerprint) {
1541 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1542 if (ids == null) {
1543 return false;
1544 }
1545 for (JsonIdentityKeyStore.Identity id : ids) {
1546 if (!Arrays.equals(id.getIdentityKey().serialize(), fingerprint)) {
1547 continue;
1548 }
1549
1550 signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
1551 save();
1552 return true;
1553 }
1554 return false;
1555 }
1556
1557 /**
1558 * Trust this the identity with this safety number
1559 *
1560 * @param name username of the identity
1561 * @param safetyNumber Safety number
1562 */
1563 public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) {
1564 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1565 if (ids == null) {
1566 return false;
1567 }
1568 for (JsonIdentityKeyStore.Identity id : ids) {
1569 if (!safetyNumber.equals(computeSafetyNumber(name, id.getIdentityKey()))) {
1570 continue;
1571 }
1572
1573 signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
1574 save();
1575 return true;
1576 }
1577 return false;
1578 }
1579
1580 /**
1581 * Trust all keys of this identity without verification
1582 *
1583 * @param name username of the identity
1584 */
1585 public boolean trustIdentityAllKeys(String name) {
1586 List<JsonIdentityKeyStore.Identity> ids = signalProtocolStore.getIdentities(name);
1587 if (ids == null) {
1588 return false;
1589 }
1590 for (JsonIdentityKeyStore.Identity id : ids) {
1591 if (id.getTrustLevel() == TrustLevel.UNTRUSTED) {
1592 signalProtocolStore.saveIdentity(name, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
1593 }
1594 }
1595 save();
1596 return true;
1597 }
1598
1599 public String computeSafetyNumber(String theirUsername, IdentityKey theirIdentityKey) {
1600 Fingerprint fingerprint = new NumericFingerprintGenerator(5200).createFor(username, getIdentity(), theirUsername, theirIdentityKey);
1601 return fingerprint.getDisplayableFingerprint().getDisplayText();
1602 }
1603 }