]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
Refresh prekeys if there are less than 20 left on the server
[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.databind.DeserializationFeature;
22 import com.fasterxml.jackson.databind.JsonNode;
23 import com.fasterxml.jackson.databind.ObjectMapper;
24 import com.fasterxml.jackson.databind.SerializationFeature;
25 import com.fasterxml.jackson.databind.node.ObjectNode;
26 import org.asamk.Signal;
27 import org.whispersystems.libsignal.*;
28 import org.whispersystems.libsignal.ecc.Curve;
29 import org.whispersystems.libsignal.ecc.ECKeyPair;
30 import org.whispersystems.libsignal.state.PreKeyRecord;
31 import org.whispersystems.libsignal.state.SignalProtocolStore;
32 import org.whispersystems.libsignal.state.SignedPreKeyRecord;
33 import org.whispersystems.libsignal.util.KeyHelper;
34 import org.whispersystems.libsignal.util.Medium;
35 import org.whispersystems.libsignal.util.guava.Optional;
36 import org.whispersystems.signalservice.api.SignalServiceAccountManager;
37 import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
38 import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
39 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
40 import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
41 import org.whispersystems.signalservice.api.messages.*;
42 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
43 import org.whispersystems.signalservice.api.push.TrustStore;
44 import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
45 import org.whispersystems.signalservice.api.util.InvalidNumberException;
46 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
47
48 import java.io.*;
49 import java.nio.file.Files;
50 import java.nio.file.Paths;
51 import java.util.*;
52 import java.util.concurrent.TimeUnit;
53 import java.util.concurrent.TimeoutException;
54
55 class Manager implements Signal {
56 private final static String URL = "https://textsecure-service.whispersystems.org";
57 private final static TrustStore TRUST_STORE = new WhisperTrustStore();
58
59 public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
60 public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
61 private final static String USER_AGENT = PROJECT_NAME + " " + PROJECT_VERSION;
62
63 private final static int PREKEY_MINIMUM_COUNT = 20;
64 private static final int PREKEY_BATCH_SIZE = 100;
65
66 private final String settingsPath;
67 private final String dataPath;
68 private final String attachmentsPath;
69
70 private final ObjectMapper jsonProcessot = new ObjectMapper();
71 private String username;
72 private String password;
73 private String signalingKey;
74 private int preKeyIdOffset;
75 private int nextSignedPreKeyId;
76
77 private boolean registered = false;
78
79 private SignalProtocolStore signalProtocolStore;
80 private SignalServiceAccountManager accountManager;
81 private JsonGroupStore groupStore;
82
83 public Manager(String username, String settingsPath) {
84 this.username = username;
85 this.settingsPath = settingsPath;
86 this.dataPath = this.settingsPath + "/data";
87 this.attachmentsPath = this.settingsPath + "/attachments";
88
89 jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
90 jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
91 jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
92 jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
93 }
94
95 public String getFileName() {
96 new File(dataPath).mkdirs();
97 return dataPath + "/" + username;
98 }
99
100 public boolean userExists() {
101 File f = new File(getFileName());
102 return !(!f.exists() || f.isDirectory());
103 }
104
105 public boolean userHasKeys() {
106 return signalProtocolStore != null;
107 }
108
109 private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
110 JsonNode node = parent.get(name);
111 if (node == null) {
112 throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
113 }
114
115 return node;
116 }
117
118 public void load() throws IOException, InvalidKeyException {
119 JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
120
121 username = getNotNullNode(rootNode, "username").asText();
122 password = getNotNullNode(rootNode, "password").asText();
123 if (rootNode.has("signalingKey")) {
124 signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
125 }
126 if (rootNode.has("preKeyIdOffset")) {
127 preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
128 } else {
129 preKeyIdOffset = 0;
130 }
131 if (rootNode.has("nextSignedPreKeyId")) {
132 nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
133 } else {
134 nextSignedPreKeyId = 0;
135 }
136 signalProtocolStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
137 registered = getNotNullNode(rootNode, "registered").asBoolean();
138 JsonNode groupStoreNode = rootNode.get("groupStore");
139 if (groupStoreNode != null) {
140 groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class);
141 }
142 if (groupStore == null) {
143 groupStore = new JsonGroupStore();
144 }
145 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
146 if (accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) {
147 refreshPreKeys();
148 save();
149 }
150 }
151
152 private void save() {
153 ObjectNode rootNode = jsonProcessot.createObjectNode();
154 rootNode.put("username", username)
155 .put("password", password)
156 .put("signalingKey", signalingKey)
157 .put("preKeyIdOffset", preKeyIdOffset)
158 .put("nextSignedPreKeyId", nextSignedPreKeyId)
159 .put("registered", registered)
160 .putPOJO("axolotlStore", signalProtocolStore)
161 .putPOJO("groupStore", groupStore)
162 ;
163 try {
164 jsonProcessot.writeValue(new File(getFileName()), rootNode);
165 } catch (Exception e) {
166 System.err.println(String.format("Error saving file: %s", e.getMessage()));
167 }
168 }
169
170 public void createNewIdentity() {
171 IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
172 int registrationId = KeyHelper.generateRegistrationId(false);
173 signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
174 groupStore = new JsonGroupStore();
175 registered = false;
176 save();
177 }
178
179 public boolean isRegistered() {
180 return registered;
181 }
182
183 public void register(boolean voiceVerication) throws IOException {
184 password = Util.getSecret(18);
185
186 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
187
188 if (voiceVerication)
189 accountManager.requestVoiceVerificationCode();
190 else
191 accountManager.requestSmsVerificationCode();
192
193 registered = false;
194 save();
195 }
196
197 private List<PreKeyRecord> generatePreKeys() {
198 List<PreKeyRecord> records = new LinkedList<>();
199
200 for (int i = 0; i < PREKEY_BATCH_SIZE; i++) {
201 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
202 ECKeyPair keyPair = Curve.generateKeyPair();
203 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
204
205 signalProtocolStore.storePreKey(preKeyId, record);
206 records.add(record);
207 }
208
209 preKeyIdOffset = (preKeyIdOffset + PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE;
210 save();
211
212 return records;
213 }
214
215 private PreKeyRecord getOrGenerateLastResortPreKey() {
216 if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) {
217 try {
218 return signalProtocolStore.loadPreKey(Medium.MAX_VALUE);
219 } catch (InvalidKeyIdException e) {
220 signalProtocolStore.removePreKey(Medium.MAX_VALUE);
221 }
222 }
223
224 ECKeyPair keyPair = Curve.generateKeyPair();
225 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
226
227 signalProtocolStore.storePreKey(Medium.MAX_VALUE, record);
228 save();
229
230 return record;
231 }
232
233 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
234 try {
235 ECKeyPair keyPair = Curve.generateKeyPair();
236 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
237 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
238
239 signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record);
240 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
241 save();
242
243 return record;
244 } catch (InvalidKeyException e) {
245 throw new AssertionError(e);
246 }
247 }
248
249 public void verifyAccount(String verificationCode) throws IOException {
250 verificationCode = verificationCode.replace("-", "");
251 signalingKey = Util.getSecret(52);
252 accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, true);
253
254 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
255 registered = true;
256
257 refreshPreKeys();
258 save();
259 }
260
261 private void refreshPreKeys() throws IOException {
262 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
263 PreKeyRecord lastResortKey = getOrGenerateLastResortPreKey();
264 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair());
265
266 accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
267 }
268
269
270 private static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
271 List<SignalServiceAttachment> SignalServiceAttachments = null;
272 if (attachments != null) {
273 SignalServiceAttachments = new ArrayList<>(attachments.size());
274 for (String attachment : attachments) {
275 try {
276 SignalServiceAttachments.add(createAttachment(attachment));
277 } catch (IOException e) {
278 throw new AttachmentInvalidException(attachment, e);
279 }
280 }
281 }
282 return SignalServiceAttachments;
283 }
284
285 private static SignalServiceAttachmentStream createAttachment(String attachment) throws IOException {
286 File attachmentFile = new File(attachment);
287 InputStream attachmentStream = new FileInputStream(attachmentFile);
288 final long attachmentSize = attachmentFile.length();
289 String mime = Files.probeContentType(Paths.get(attachment));
290 return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null);
291 }
292
293 @Override
294 public void sendGroupMessage(String messageText, List<String> attachments,
295 byte[] groupId)
296 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
297 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
298 if (attachments != null) {
299 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
300 }
301 if (groupId != null) {
302 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
303 .withId(groupId)
304 .build();
305 messageBuilder.asGroupMessage(group);
306 }
307 SignalServiceDataMessage message = messageBuilder.build();
308
309 sendMessage(message, groupStore.getGroup(groupId).members);
310 }
311
312 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
313 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
314 .withId(groupId)
315 .build();
316
317 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
318 .asGroupMessage(group)
319 .build();
320
321 sendMessage(message, groupStore.getGroup(groupId).members);
322 }
323
324 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
325 GroupInfo g;
326 if (groupId == null) {
327 // Create new group
328 g = new GroupInfo(Util.getSecretBytes(16));
329 g.members.add(username);
330 } else {
331 g = groupStore.getGroup(groupId);
332 }
333
334 if (name != null) {
335 g.name = name;
336 }
337
338 if (members != null) {
339 for (String member : members) {
340 try {
341 g.members.add(canonicalizeNumber(member));
342 } catch (InvalidNumberException e) {
343 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
344 System.err.println("Aborting…");
345 System.exit(1);
346 }
347 }
348 }
349
350 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
351 .withId(g.groupId)
352 .withName(g.name)
353 .withMembers(new ArrayList<>(g.members));
354
355 if (avatarFile != null) {
356 try {
357 group.withAvatar(createAttachment(avatarFile));
358 // TODO
359 g.avatarId = 0;
360 } catch (IOException e) {
361 throw new AttachmentInvalidException(avatarFile, e);
362 }
363 }
364
365 groupStore.updateGroup(g);
366
367 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
368 .asGroupMessage(group.build())
369 .build();
370
371 sendMessage(message, g.members);
372 return g.groupId;
373 }
374
375 @Override
376 public void sendMessage(String message, List<String> attachments, String recipient)
377 throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
378 List<String> recipients = new ArrayList<>(1);
379 recipients.add(recipient);
380 sendMessage(message, attachments, recipients);
381 }
382
383 @Override
384 public void sendMessage(String messageText, List<String> attachments,
385 List<String> recipients)
386 throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
387 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
388 if (attachments != null) {
389 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
390 }
391 SignalServiceDataMessage message = messageBuilder.build();
392
393 sendMessage(message, recipients);
394 }
395
396 @Override
397 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
398 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
399 .asEndSessionMessage()
400 .build();
401
402 sendMessage(message, recipients);
403 }
404
405 private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
406 throws IOException, EncapsulatedExceptions {
407 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
408 signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
409
410 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
411 for (String recipient : recipients) {
412 try {
413 recipientsTS.add(getPushAddress(recipient));
414 } catch (InvalidNumberException e) {
415 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
416 System.err.println("Aborting sending.");
417 save();
418 return;
419 }
420 }
421
422 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
423
424 if (message.isEndSession()) {
425 for (SignalServiceAddress recipient : recipientsTS) {
426 handleEndSession(recipient.getNumber());
427 }
428 }
429 save();
430 }
431
432 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) {
433 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
434 try {
435 return cipher.decrypt(envelope);
436 } catch (Exception e) {
437 // TODO handle all exceptions
438 e.printStackTrace();
439 return null;
440 }
441 }
442
443 private void handleEndSession(String source) {
444 signalProtocolStore.deleteAllSessions(source);
445 }
446
447 public interface ReceiveMessageHandler {
448 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, GroupInfo group);
449 }
450
451 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
452 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
453 SignalServiceMessagePipe messagePipe = null;
454
455 try {
456 messagePipe = messageReceiver.createMessagePipe();
457
458 while (true) {
459 SignalServiceEnvelope envelope;
460 SignalServiceContent content = null;
461 GroupInfo group = null;
462 try {
463 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
464 if (!envelope.isReceipt()) {
465 content = decryptMessage(envelope);
466 if (content != null) {
467 if (content.getDataMessage().isPresent()) {
468 SignalServiceDataMessage message = content.getDataMessage().get();
469 if (message.getGroupInfo().isPresent()) {
470 SignalServiceGroup groupInfo = message.getGroupInfo().get();
471 switch (groupInfo.getType()) {
472 case UPDATE:
473 try {
474 group = groupStore.getGroup(groupInfo.getGroupId());
475 } catch (GroupNotFoundException e) {
476 group = new GroupInfo(groupInfo.getGroupId());
477 }
478
479 if (groupInfo.getAvatar().isPresent()) {
480 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
481 if (avatar.isPointer()) {
482 long avatarId = avatar.asPointer().getId();
483 try {
484 retrieveAttachment(avatar.asPointer());
485 group.avatarId = avatarId;
486 } catch (IOException | InvalidMessageException e) {
487 System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
488 }
489 }
490 }
491
492 if (groupInfo.getName().isPresent()) {
493 group.name = groupInfo.getName().get();
494 }
495
496 if (groupInfo.getMembers().isPresent()) {
497 group.members.addAll(groupInfo.getMembers().get());
498 }
499
500 groupStore.updateGroup(group);
501 break;
502 case DELIVER:
503 try {
504 group = groupStore.getGroup(groupInfo.getGroupId());
505 } catch (GroupNotFoundException e) {
506 }
507 break;
508 case QUIT:
509 try {
510 group = groupStore.getGroup(groupInfo.getGroupId());
511 group.members.remove(envelope.getSource());
512 } catch (GroupNotFoundException e) {
513 }
514 break;
515 }
516 }
517 if (message.isEndSession()) {
518 handleEndSession(envelope.getSource());
519 }
520 if (message.getAttachments().isPresent()) {
521 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
522 if (attachment.isPointer()) {
523 try {
524 retrieveAttachment(attachment.asPointer());
525 } catch (IOException | InvalidMessageException e) {
526 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
527 }
528 }
529 }
530 }
531 }
532 }
533 }
534 save();
535 handler.handleMessage(envelope, content, group);
536 } catch (TimeoutException e) {
537 if (returnOnTimeout)
538 return;
539 } catch (InvalidVersionException e) {
540 System.err.println("Ignoring error: " + e.getMessage());
541 }
542 }
543 } finally {
544 if (messagePipe != null)
545 messagePipe.shutdown();
546 }
547 }
548
549 public File getAttachmentFile(long attachmentId) {
550 return new File(attachmentsPath, attachmentId + "");
551 }
552
553 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
554 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
555
556 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
557 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
558
559 new File(attachmentsPath).mkdirs();
560 File outputFile = getAttachmentFile(pointer.getId());
561 OutputStream output = null;
562 try {
563 output = new FileOutputStream(outputFile);
564 byte[] buffer = new byte[4096];
565 int read;
566
567 while ((read = input.read(buffer)) != -1) {
568 output.write(buffer, 0, read);
569 }
570 } catch (FileNotFoundException e) {
571 e.printStackTrace();
572 return null;
573 } finally {
574 if (output != null) {
575 output.close();
576 output = null;
577 }
578 if (!tmpFile.delete()) {
579 System.err.println("Failed to delete temp file: " + tmpFile);
580 }
581 }
582 if (pointer.getPreview().isPresent()) {
583 File previewFile = new File(outputFile + ".preview");
584 try {
585 output = new FileOutputStream(previewFile);
586 byte[] preview = pointer.getPreview().get();
587 output.write(preview, 0, preview.length);
588 } catch (FileNotFoundException e) {
589 e.printStackTrace();
590 return null;
591 } finally {
592 if (output != null) {
593 output.close();
594 }
595 }
596 }
597 return outputFile;
598 }
599
600 private String canonicalizeNumber(String number) throws InvalidNumberException {
601 String localNumber = username;
602 return PhoneNumberFormatter.formatNumber(number, localNumber);
603 }
604
605 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
606 String e164number = canonicalizeNumber(number);
607 return new SignalServiceAddress(e164number);
608 }
609
610 @Override
611 public boolean isRemote() {
612 return false;
613 }
614 }