]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Main.java
Update dependency
[signal-cli] / src / main / java / org / asamk / signal / Main.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 net.sourceforge.argparse4j.ArgumentParsers;
20 import net.sourceforge.argparse4j.impl.Arguments;
21 import net.sourceforge.argparse4j.inf.*;
22 import org.apache.http.util.TextUtils;
23 import org.asamk.Signal;
24 import org.freedesktop.dbus.DBusConnection;
25 import org.freedesktop.dbus.DBusSigHandler;
26 import org.freedesktop.dbus.exceptions.DBusException;
27 import org.freedesktop.dbus.exceptions.DBusExecutionException;
28 import org.whispersystems.libsignal.InvalidKeyException;
29 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
30 import org.whispersystems.signalservice.api.messages.*;
31 import org.whispersystems.signalservice.api.messages.multidevice.*;
32 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
33 import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
34 import org.whispersystems.signalservice.api.push.exceptions.NetworkFailureException;
35 import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
36 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
37
38 import java.io.File;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.io.StringWriter;
42 import java.net.URI;
43 import java.net.URISyntaxException;
44 import java.nio.charset.Charset;
45 import java.security.Security;
46 import java.text.DateFormat;
47 import java.text.SimpleDateFormat;
48 import java.util.*;
49 import java.util.concurrent.TimeoutException;
50
51 public class Main {
52
53 public static final String SIGNAL_BUSNAME = "org.asamk.Signal";
54 public static final String SIGNAL_OBJECTPATH = "/org/asamk/Signal";
55
56 private static final TimeZone tzUTC = TimeZone.getTimeZone("UTC");
57
58 public static void main(String[] args) {
59 // Workaround for BKS truststore
60 Security.insertProviderAt(new org.bouncycastle.jce.provider.BouncyCastleProvider(), 1);
61
62 Namespace ns = parseArgs(args);
63 if (ns == null) {
64 System.exit(1);
65 }
66
67 int res = handleCommands(ns);
68 System.exit(res);
69 }
70
71 private static int handleCommands(Namespace ns) {
72 final String username = ns.getString("username");
73 Manager m;
74 Signal ts;
75 DBusConnection dBusConn = null;
76 try {
77 if (ns.getBoolean("dbus") || ns.getBoolean("dbus_system")) {
78 try {
79 m = null;
80 int busType;
81 if (ns.getBoolean("dbus_system")) {
82 busType = DBusConnection.SYSTEM;
83 } else {
84 busType = DBusConnection.SESSION;
85 }
86 dBusConn = DBusConnection.getConnection(busType);
87 ts = (Signal) dBusConn.getRemoteObject(
88 SIGNAL_BUSNAME, SIGNAL_OBJECTPATH,
89 Signal.class);
90 } catch (DBusException e) {
91 e.printStackTrace();
92 if (dBusConn != null) {
93 dBusConn.disconnect();
94 }
95 return 3;
96 }
97 } else {
98 String settingsPath = ns.getString("config");
99 if (TextUtils.isEmpty(settingsPath)) {
100 settingsPath = System.getProperty("user.home") + "/.config/signal";
101 if (!new File(settingsPath).exists()) {
102 String legacySettingsPath = System.getProperty("user.home") + "/.config/textsecure";
103 if (new File(legacySettingsPath).exists()) {
104 settingsPath = legacySettingsPath;
105 }
106 }
107 }
108
109 m = new Manager(username, settingsPath);
110 ts = m;
111 if (m.userExists()) {
112 try {
113 m.init();
114 } catch (Exception e) {
115 System.err.println("Error loading state file \"" + m.getFileName() + "\": " + e.getMessage());
116 return 2;
117 }
118 }
119 }
120
121 switch (ns.getString("command")) {
122 case "register":
123 if (dBusConn != null) {
124 System.err.println("register is not yet implemented via dbus");
125 return 1;
126 }
127 if (!m.userHasKeys()) {
128 m.createNewIdentity();
129 }
130 try {
131 m.register(ns.getBoolean("voice"));
132 } catch (IOException e) {
133 System.err.println("Request verify error: " + e.getMessage());
134 return 3;
135 }
136 break;
137 case "verify":
138 if (dBusConn != null) {
139 System.err.println("verify is not yet implemented via dbus");
140 return 1;
141 }
142 if (!m.userHasKeys()) {
143 System.err.println("User has no keys, first call register.");
144 return 1;
145 }
146 if (m.isRegistered()) {
147 System.err.println("User registration is already verified");
148 return 1;
149 }
150 try {
151 m.verifyAccount(ns.getString("verificationCode"));
152 } catch (IOException e) {
153 System.err.println("Verify error: " + e.getMessage());
154 return 3;
155 }
156 break;
157 case "link":
158 if (dBusConn != null) {
159 System.err.println("link is not yet implemented via dbus");
160 return 1;
161 }
162
163 // When linking, username is null and we always have to create keys
164 m.createNewIdentity();
165
166 String deviceName = ns.getString("name");
167 if (deviceName == null) {
168 deviceName = "cli";
169 }
170 try {
171 System.out.println(m.getDeviceLinkUri());
172 m.finishDeviceLink(deviceName);
173 System.out.println("Associated with: " + m.getUsername());
174 } catch (TimeoutException e) {
175 System.err.println("Link request timed out, please try again.");
176 return 3;
177 } catch (IOException e) {
178 System.err.println("Link request error: " + e.getMessage());
179 return 3;
180 } catch (AssertionError e) {
181 handleAssertionError(e);
182 return 1;
183 } catch (InvalidKeyException e) {
184 e.printStackTrace();
185 return 2;
186 } catch (UserAlreadyExists e) {
187 System.err.println("The user " + e.getUsername() + " already exists\nDelete \"" + e.getFileName() + "\" before trying again.");
188 return 1;
189 }
190 break;
191 case "addDevice":
192 if (dBusConn != null) {
193 System.err.println("link is not yet implemented via dbus");
194 return 1;
195 }
196 if (!m.isRegistered()) {
197 System.err.println("User is not registered.");
198 return 1;
199 }
200 try {
201 m.addDeviceLink(new URI(ns.getString("uri")));
202 } catch (IOException e) {
203 e.printStackTrace();
204 return 3;
205 } catch (InvalidKeyException e) {
206 e.printStackTrace();
207 return 2;
208 } catch (AssertionError e) {
209 handleAssertionError(e);
210 return 1;
211 } catch (URISyntaxException e) {
212 e.printStackTrace();
213 return 2;
214 }
215 break;
216 case "listDevices":
217 if (dBusConn != null) {
218 System.err.println("listDevices is not yet implemented via dbus");
219 return 1;
220 }
221 if (!m.isRegistered()) {
222 System.err.println("User is not registered.");
223 return 1;
224 }
225 try {
226 List<DeviceInfo> devices = m.getLinkedDevices();
227 for (DeviceInfo d : devices) {
228 System.out.println("Device " + d.getId() + (d.getId() == m.getDeviceId() ? " (this device)" : "") + ":");
229 System.out.println(" Name: " + d.getName());
230 System.out.println(" Created: " + d.getCreated());
231 System.out.println(" Last seen: " + d.getLastSeen());
232 }
233 } catch (IOException e) {
234 e.printStackTrace();
235 return 3;
236 }
237 break;
238 case "removeDevice":
239 if (dBusConn != null) {
240 System.err.println("removeDevice is not yet implemented via dbus");
241 return 1;
242 }
243 if (!m.isRegistered()) {
244 System.err.println("User is not registered.");
245 return 1;
246 }
247 try {
248 int deviceId = ns.getInt("deviceId");
249 m.removeLinkedDevices(deviceId);
250 } catch (IOException e) {
251 e.printStackTrace();
252 return 3;
253 }
254 break;
255 case "send":
256 if (dBusConn == null && !m.isRegistered()) {
257 System.err.println("User is not registered.");
258 return 1;
259 }
260
261 if (ns.getBoolean("endsession")) {
262 if (ns.getList("recipient") == null) {
263 System.err.println("No recipients given");
264 System.err.println("Aborting sending.");
265 return 1;
266 }
267 try {
268 ts.sendEndSessionMessage(ns.<String>getList("recipient"));
269 } catch (IOException e) {
270 handleIOException(e);
271 return 3;
272 } catch (EncapsulatedExceptions e) {
273 handleEncapsulatedExceptions(e);
274 return 3;
275 } catch (AssertionError e) {
276 handleAssertionError(e);
277 return 1;
278 } catch (DBusExecutionException e) {
279 handleDBusExecutionException(e);
280 return 1;
281 }
282 } else {
283 String messageText = ns.getString("message");
284 if (messageText == null) {
285 try {
286 messageText = readAll(System.in);
287 } catch (IOException e) {
288 System.err.println("Failed to read message from stdin: " + e.getMessage());
289 System.err.println("Aborting sending.");
290 return 1;
291 }
292 }
293
294 try {
295 List<String> attachments = ns.getList("attachment");
296 if (attachments == null) {
297 attachments = new ArrayList<>();
298 }
299 if (ns.getString("group") != null) {
300 byte[] groupId = decodeGroupId(ns.getString("group"));
301 ts.sendGroupMessage(messageText, attachments, groupId);
302 } else {
303 ts.sendMessage(messageText, attachments, ns.<String>getList("recipient"));
304 }
305 } catch (IOException e) {
306 handleIOException(e);
307 return 3;
308 } catch (EncapsulatedExceptions e) {
309 handleEncapsulatedExceptions(e);
310 return 3;
311 } catch (AssertionError e) {
312 handleAssertionError(e);
313 return 1;
314 } catch (GroupNotFoundException e) {
315 handleGroupNotFoundException(e);
316 return 1;
317 } catch (NotAGroupMemberException e) {
318 handleNotAGroupMemberException(e);
319 return 1;
320 } catch (AttachmentInvalidException e) {
321 System.err.println("Failed to add attachment: " + e.getMessage());
322 System.err.println("Aborting sending.");
323 return 1;
324 } catch (DBusExecutionException e) {
325 handleDBusExecutionException(e);
326 return 1;
327 }
328 }
329
330 break;
331 case "receive":
332 if (dBusConn != null) {
333 try {
334 dBusConn.addSigHandler(Signal.MessageReceived.class, new DBusSigHandler<Signal.MessageReceived>() {
335 @Override
336 public void handle(Signal.MessageReceived s) {
337 System.out.print(String.format("Envelope from: %s\nTimestamp: %s\nBody: %s\n",
338 s.getSender(), formatTimestamp(s.getTimestamp()), s.getMessage()));
339 if (s.getGroupId().length > 0) {
340 System.out.println("Group info:");
341 System.out.println(" Id: " + Base64.encodeBytes(s.getGroupId()));
342 }
343 if (s.getAttachments().size() > 0) {
344 System.out.println("Attachments: ");
345 for (String attachment : s.getAttachments()) {
346 System.out.println("- Stored plaintext in: " + attachment);
347 }
348 }
349 System.out.println();
350 }
351 });
352 } catch (DBusException e) {
353 e.printStackTrace();
354 return 1;
355 }
356 while (true) {
357 try {
358 Thread.sleep(10000);
359 } catch (InterruptedException e) {
360 return 0;
361 }
362 }
363 }
364 if (!m.isRegistered()) {
365 System.err.println("User is not registered.");
366 return 1;
367 }
368 int timeout = 5;
369 if (ns.getInt("timeout") != null) {
370 timeout = ns.getInt("timeout");
371 }
372 boolean returnOnTimeout = true;
373 if (timeout < 0) {
374 returnOnTimeout = false;
375 timeout = 3600;
376 }
377 try {
378 m.receiveMessages(timeout, returnOnTimeout, new ReceiveMessageHandler(m));
379 } catch (IOException e) {
380 System.err.println("Error while receiving messages: " + e.getMessage());
381 return 3;
382 } catch (AssertionError e) {
383 handleAssertionError(e);
384 return 1;
385 }
386 break;
387 case "quitGroup":
388 if (dBusConn != null) {
389 System.err.println("quitGroup is not yet implemented via dbus");
390 return 1;
391 }
392 if (!m.isRegistered()) {
393 System.err.println("User is not registered.");
394 return 1;
395 }
396
397 try {
398 m.sendQuitGroupMessage(decodeGroupId(ns.getString("group")));
399 } catch (IOException e) {
400 handleIOException(e);
401 return 3;
402 } catch (EncapsulatedExceptions e) {
403 handleEncapsulatedExceptions(e);
404 return 3;
405 } catch (AssertionError e) {
406 handleAssertionError(e);
407 return 1;
408 } catch (GroupNotFoundException e) {
409 handleGroupNotFoundException(e);
410 return 1;
411 } catch (NotAGroupMemberException e) {
412 handleNotAGroupMemberException(e);
413 return 1;
414 }
415
416 break;
417 case "updateGroup":
418 if (dBusConn != null) {
419 System.err.println("updateGroup is not yet implemented via dbus");
420 return 1;
421 }
422 if (!m.isRegistered()) {
423 System.err.println("User is not registered.");
424 return 1;
425 }
426
427 try {
428 byte[] groupId = null;
429 if (ns.getString("group") != null) {
430 groupId = decodeGroupId(ns.getString("group"));
431 }
432 byte[] newGroupId = m.sendUpdateGroupMessage(groupId, ns.getString("name"), ns.<String>getList("member"), ns.getString("avatar"));
433 if (groupId == null) {
434 System.out.println("Creating new group \"" + Base64.encodeBytes(newGroupId) + "\" …");
435 }
436 } catch (IOException e) {
437 handleIOException(e);
438 return 3;
439 } catch (AttachmentInvalidException e) {
440 System.err.println("Failed to add avatar attachment for group\": " + e.getMessage());
441 System.err.println("Aborting sending.");
442 return 1;
443 } catch (GroupNotFoundException e) {
444 handleGroupNotFoundException(e);
445 return 1;
446 } catch (NotAGroupMemberException e) {
447 handleNotAGroupMemberException(e);
448 return 1;
449 } catch (EncapsulatedExceptions e) {
450 handleEncapsulatedExceptions(e);
451 return 3;
452 }
453
454 break;
455 case "listIdentities":
456 if (dBusConn != null) {
457 System.err.println("listIdentities is not yet implemented via dbus");
458 return 1;
459 }
460 if (!m.isRegistered()) {
461 System.err.println("User is not registered.");
462 return 1;
463 }
464 if (ns.get("number") == null) {
465 for (Map.Entry<String, List<JsonIdentityKeyStore.Identity>> keys : m.getIdentities().entrySet()) {
466 for (JsonIdentityKeyStore.Identity id : keys.getValue()) {
467 printIdentityFingerprint(m, keys.getKey(), id);
468 }
469 }
470 } else {
471 String number = ns.getString("number");
472 for (JsonIdentityKeyStore.Identity id : m.getIdentities(number)) {
473 printIdentityFingerprint(m, number, id);
474 }
475 }
476 break;
477 case "trust":
478 if (dBusConn != null) {
479 System.err.println("trust is not yet implemented via dbus");
480 return 1;
481 }
482 if (!m.isRegistered()) {
483 System.err.println("User is not registered.");
484 return 1;
485 }
486 String number = ns.getString("number");
487 if (ns.getBoolean("trust_all_known_keys")) {
488 boolean res = m.trustIdentityAllKeys(number);
489 if (!res) {
490 System.err.println("Failed to set the trust for this number, make sure the number is correct.");
491 return 1;
492 }
493 } else {
494 String fingerprint = ns.getString("verified_fingerprint");
495 if (fingerprint != null) {
496 fingerprint = fingerprint.replaceAll(" ", "");
497 if (fingerprint.length() == 66) {
498 byte[] fingerprintBytes;
499 try {
500 fingerprintBytes = Hex.toByteArray(fingerprint.toLowerCase(Locale.ROOT));
501 } catch (Exception e) {
502 System.err.println("Failed to parse the fingerprint, make sure the fingerprint is a correctly encoded hex string without additional characters.");
503 return 1;
504 }
505 boolean res = m.trustIdentityVerified(number, fingerprintBytes);
506 if (!res) {
507 System.err.println("Failed to set the trust for the fingerprint of this number, make sure the number and the fingerprint are correct.");
508 return 1;
509 }
510 } else if (fingerprint.length() == 60) {
511 boolean res = m.trustIdentityVerifiedSafetyNumber(number, fingerprint);
512 if (!res) {
513 System.err.println("Failed to set the trust for the safety number of this phone number, make sure the phone number and the safety number are correct.");
514 return 1;
515 }
516 } else {
517 System.err.println("Fingerprint has invalid format, either specify the old hex fingerprint or the new safety number");
518 return 1;
519 }
520 } else {
521 System.err.println("You need to specify the fingerprint you have verified with -v FINGERPRINT");
522 return 1;
523 }
524 }
525 break;
526 case "daemon":
527 if (dBusConn != null) {
528 System.err.println("Stop it.");
529 return 1;
530 }
531 if (!m.isRegistered()) {
532 System.err.println("User is not registered.");
533 return 1;
534 }
535 DBusConnection conn = null;
536 try {
537 try {
538 int busType;
539 if (ns.getBoolean("system")) {
540 busType = DBusConnection.SYSTEM;
541 } else {
542 busType = DBusConnection.SESSION;
543 }
544 conn = DBusConnection.getConnection(busType);
545 conn.exportObject(SIGNAL_OBJECTPATH, m);
546 conn.requestBusName(SIGNAL_BUSNAME);
547 } catch (DBusException e) {
548 e.printStackTrace();
549 return 2;
550 }
551 try {
552 m.receiveMessages(3600, false, new DbusReceiveMessageHandler(m, conn));
553 } catch (IOException e) {
554 System.err.println("Error while receiving messages: " + e.getMessage());
555 return 3;
556 } catch (AssertionError e) {
557 handleAssertionError(e);
558 return 1;
559 }
560 } finally {
561 if (conn != null) {
562 conn.disconnect();
563 }
564 }
565
566 break;
567 }
568 return 0;
569 } finally {
570 if (dBusConn != null) {
571 dBusConn.disconnect();
572 }
573 }
574 }
575
576 private static void printIdentityFingerprint(Manager m, String theirUsername, JsonIdentityKeyStore.Identity theirId) {
577 String digits = formatSafetyNumber(m.computeSafetyNumber(theirUsername, theirId.identityKey));
578 System.out.println(String.format("%s: %s Added: %s Fingerprint: %s Safety Number: %s", theirUsername,
579 theirId.trustLevel, theirId.added, Hex.toStringCondensed(theirId.getFingerprint()), digits));
580 }
581
582 private static String formatSafetyNumber(String digits) {
583 final int partCount = 12;
584 int partSize = digits.length() / partCount;
585 StringBuilder f = new StringBuilder(digits.length() + partCount);
586 for (int i = 0; i < partCount; i++) {
587 f.append(digits.substring(i * partSize, (i * partSize) + partSize)).append(" ");
588 }
589 return f.toString();
590 }
591
592 private static void handleGroupNotFoundException(GroupNotFoundException e) {
593 System.err.println("Failed to send to group: " + e.getMessage());
594 System.err.println("Aborting sending.");
595 }
596
597 private static void handleNotAGroupMemberException(NotAGroupMemberException e) {
598 System.err.println("Failed to send to group: " + e.getMessage());
599 System.err.println("Update the group on another device to readd the user to this group.");
600 System.err.println("Aborting sending.");
601 }
602
603
604 private static void handleDBusExecutionException(DBusExecutionException e) {
605 System.err.println("Cannot connect to dbus: " + e.getMessage());
606 System.err.println("Aborting.");
607 }
608
609 private static byte[] decodeGroupId(String groupId) {
610 try {
611 return Base64.decode(groupId);
612 } catch (IOException e) {
613 System.err.println("Failed to decode groupId (must be base64) \"" + groupId + "\": " + e.getMessage());
614 System.err.println("Aborting sending.");
615 System.exit(1);
616 return null;
617 }
618 }
619
620 private static Namespace parseArgs(String[] args) {
621 ArgumentParser parser = ArgumentParsers.newArgumentParser("signal-cli")
622 .defaultHelp(true)
623 .description("Commandline interface for Signal.")
624 .version(Manager.PROJECT_NAME + " " + Manager.PROJECT_VERSION);
625
626 parser.addArgument("-v", "--version")
627 .help("Show package version.")
628 .action(Arguments.version());
629 parser.addArgument("--config")
630 .help("Set the path, where to store the config (Default: $HOME/.config/signal).");
631
632 MutuallyExclusiveGroup mut = parser.addMutuallyExclusiveGroup();
633 mut.addArgument("-u", "--username")
634 .help("Specify your phone number, that will be used for verification.");
635 mut.addArgument("--dbus")
636 .help("Make request via user dbus.")
637 .action(Arguments.storeTrue());
638 mut.addArgument("--dbus-system")
639 .help("Make request via system dbus.")
640 .action(Arguments.storeTrue());
641
642 Subparsers subparsers = parser.addSubparsers()
643 .title("subcommands")
644 .dest("command")
645 .description("valid subcommands")
646 .help("additional help");
647
648 Subparser parserLink = subparsers.addParser("link");
649 parserLink.addArgument("-n", "--name")
650 .help("Specify a name to describe this new device.");
651
652 Subparser parserAddDevice = subparsers.addParser("addDevice");
653 parserAddDevice.addArgument("--uri")
654 .required(true)
655 .help("Specify the uri contained in the QR code shown by the new device.");
656
657 Subparser parserDevices = subparsers.addParser("listDevices");
658
659 Subparser parserRemoveDevice = subparsers.addParser("removeDevice");
660 parserRemoveDevice.addArgument("-d", "--deviceId")
661 .type(int.class)
662 .required(true)
663 .help("Specify the device you want to remove. Use listDevices to see the deviceIds.");
664
665 Subparser parserRegister = subparsers.addParser("register");
666 parserRegister.addArgument("-v", "--voice")
667 .help("The verification should be done over voice, not sms.")
668 .action(Arguments.storeTrue());
669
670 Subparser parserVerify = subparsers.addParser("verify");
671 parserVerify.addArgument("verificationCode")
672 .help("The verification code you received via sms or voice call.");
673
674 Subparser parserSend = subparsers.addParser("send");
675 parserSend.addArgument("-g", "--group")
676 .help("Specify the recipient group ID.");
677 parserSend.addArgument("recipient")
678 .help("Specify the recipients' phone number.")
679 .nargs("*");
680 parserSend.addArgument("-m", "--message")
681 .help("Specify the message, if missing standard input is used.");
682 parserSend.addArgument("-a", "--attachment")
683 .nargs("*")
684 .help("Add file as attachment");
685 parserSend.addArgument("-e", "--endsession")
686 .help("Clear session state and send end session message.")
687 .action(Arguments.storeTrue());
688
689 Subparser parserLeaveGroup = subparsers.addParser("quitGroup");
690 parserLeaveGroup.addArgument("-g", "--group")
691 .required(true)
692 .help("Specify the recipient group ID.");
693
694 Subparser parserUpdateGroup = subparsers.addParser("updateGroup");
695 parserUpdateGroup.addArgument("-g", "--group")
696 .help("Specify the recipient group ID.");
697 parserUpdateGroup.addArgument("-n", "--name")
698 .help("Specify the new group name.");
699 parserUpdateGroup.addArgument("-a", "--avatar")
700 .help("Specify a new group avatar image file");
701 parserUpdateGroup.addArgument("-m", "--member")
702 .nargs("*")
703 .help("Specify one or more members to add to the group");
704
705 Subparser parserListIdentities = subparsers.addParser("listIdentities");
706 parserListIdentities.addArgument("-n", "--number")
707 .help("Only show identity keys for the given phone number.");
708
709 Subparser parserTrust = subparsers.addParser("trust");
710 parserTrust.addArgument("number")
711 .help("Specify the phone number, for which to set the trust.")
712 .required(true);
713 MutuallyExclusiveGroup mutTrust = parserTrust.addMutuallyExclusiveGroup();
714 mutTrust.addArgument("-a", "--trust-all-known-keys")
715 .help("Trust all known keys of this user, only use this for testing.")
716 .action(Arguments.storeTrue());
717 mutTrust.addArgument("-v", "--verified-fingerprint")
718 .help("Specify the fingerprint of the key, only use this option if you have verified the fingerprint.");
719
720 Subparser parserReceive = subparsers.addParser("receive");
721 parserReceive.addArgument("-t", "--timeout")
722 .type(int.class)
723 .help("Number of seconds to wait for new messages (negative values disable timeout)");
724
725 Subparser parserDaemon = subparsers.addParser("daemon");
726 parserDaemon.addArgument("--system")
727 .action(Arguments.storeTrue())
728 .help("Use DBus system bus instead of user bus.");
729
730 try {
731 Namespace ns = parser.parseArgs(args);
732 if ("link".equals(ns.getString("command"))) {
733 if (ns.getString("username") != null) {
734 parser.printUsage();
735 System.err.println("You cannot specify a username (phone number) when linking");
736 System.exit(2);
737 }
738 } else if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) {
739 if (ns.getString("username") == null) {
740 parser.printUsage();
741 System.err.println("You need to specify a username (phone number)");
742 System.exit(2);
743 }
744 if (!PhoneNumberFormatter.isValidNumber(ns.getString("username"))) {
745 System.err.println("Invalid username (phone number), make sure you include the country code.");
746 System.exit(2);
747 }
748 }
749 if (ns.getList("recipient") != null && !ns.getList("recipient").isEmpty() && ns.getString("group") != null) {
750 System.err.println("You cannot specify recipients by phone number and groups a the same time");
751 System.exit(2);
752 }
753 return ns;
754 } catch (ArgumentParserException e) {
755 parser.handleError(e);
756 return null;
757 }
758 }
759
760 private static void handleAssertionError(AssertionError e) {
761 System.err.println("Failed to send/receive message (Assertion): " + e.getMessage());
762 e.printStackTrace();
763 System.err.println("If you use an Oracle JRE please check if you have unlimited strength crypto enabled, see README");
764 }
765
766 private static void handleEncapsulatedExceptions(EncapsulatedExceptions e) {
767 System.err.println("Failed to send (some) messages:");
768 for (NetworkFailureException n : e.getNetworkExceptions()) {
769 System.err.println("Network failure for \"" + n.getE164number() + "\": " + n.getMessage());
770 }
771 for (UnregisteredUserException n : e.getUnregisteredUserExceptions()) {
772 System.err.println("Unregistered user \"" + n.getE164Number() + "\": " + n.getMessage());
773 }
774 for (UntrustedIdentityException n : e.getUntrustedIdentityExceptions()) {
775 System.err.println("Untrusted Identity for \"" + n.getE164Number() + "\": " + n.getMessage());
776 }
777 }
778
779 private static void handleIOException(IOException e) {
780 System.err.println("Failed to send message: " + e.getMessage());
781 }
782
783 private static String readAll(InputStream in) throws IOException {
784 StringWriter output = new StringWriter();
785 byte[] buffer = new byte[4096];
786 long count = 0;
787 int n;
788 while (-1 != (n = System.in.read(buffer))) {
789 output.write(new String(buffer, 0, n, Charset.defaultCharset()));
790 count += n;
791 }
792 return output.toString();
793 }
794
795 private static class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
796 final Manager m;
797
798 public ReceiveMessageHandler(Manager m) {
799 this.m = m;
800 }
801
802 @Override
803 public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) {
804 SignalServiceAddress source = envelope.getSourceAddress();
805 ContactInfo sourceContact = m.getContact(source.getNumber());
806 System.out.println(String.format("Envelope from: %s (device: %d)", (sourceContact == null ? "" : "“" + sourceContact.name + "” ") + source.getNumber(), envelope.getSourceDevice()));
807 if (source.getRelay().isPresent()) {
808 System.out.println("Relayed by: " + source.getRelay().get());
809 }
810 System.out.println("Timestamp: " + formatTimestamp(envelope.getTimestamp()));
811
812 if (envelope.isReceipt()) {
813 System.out.println("Got receipt.");
814 } else if (envelope.isSignalMessage() | envelope.isPreKeySignalMessage()) {
815 if (exception != null) {
816 if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
817 org.whispersystems.libsignal.UntrustedIdentityException e = (org.whispersystems.libsignal.UntrustedIdentityException) exception;
818 System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
819 System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getName() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getName() + "' to mark it as trusted");
820 System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getName() + "' to trust it without verification");
821 } else {
822 System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")");
823 }
824 }
825 if (content == null) {
826 System.out.println("Failed to decrypt message.");
827 } else {
828 if (content.getDataMessage().isPresent()) {
829 SignalServiceDataMessage message = content.getDataMessage().get();
830 handleSignalServiceDataMessage(message);
831 }
832 if (content.getSyncMessage().isPresent()) {
833 System.out.println("Received a sync message");
834 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
835
836 if (syncMessage.getContacts().isPresent()) {
837 System.out.println("Received sync contacts");
838 printAttachment(syncMessage.getContacts().get());
839 }
840 if (syncMessage.getGroups().isPresent()) {
841 System.out.println("Received sync groups");
842 printAttachment(syncMessage.getGroups().get());
843 }
844 if (syncMessage.getRead().isPresent()) {
845 System.out.println("Received sync read messages list");
846 for (ReadMessage rm : syncMessage.getRead().get()) {
847 ContactInfo fromContact = m.getContact(rm.getSender());
848 System.out.println("From: " + (fromContact == null ? "" : "“" + fromContact.name + "” ") + rm.getSender() + " Message timestamp: " + formatTimestamp(rm.getTimestamp()));
849 }
850 }
851 if (syncMessage.getRequest().isPresent()) {
852 System.out.println("Received sync request");
853 if (syncMessage.getRequest().get().isContactsRequest()) {
854 System.out.println(" - contacts request");
855 }
856 if (syncMessage.getRequest().get().isGroupsRequest()) {
857 System.out.println(" - groups request");
858 }
859 }
860 if (syncMessage.getSent().isPresent()) {
861 System.out.println("Received sync sent message");
862 final SentTranscriptMessage sentTranscriptMessage = syncMessage.getSent().get();
863 String to;
864 if (sentTranscriptMessage.getDestination().isPresent()) {
865 String dest = sentTranscriptMessage.getDestination().get();
866 ContactInfo destContact = m.getContact(dest);
867 to = (destContact == null ? "" : "“" + destContact.name + "” ") + dest;
868 } else {
869 to = "Unknown";
870 }
871 System.out.println("To: " + to + " , Message timestamp: " + formatTimestamp(sentTranscriptMessage.getTimestamp()));
872 if (sentTranscriptMessage.getExpirationStartTimestamp() > 0) {
873 System.out.println("Expiration started at: " + formatTimestamp(sentTranscriptMessage.getExpirationStartTimestamp()));
874 }
875 SignalServiceDataMessage message = sentTranscriptMessage.getMessage();
876 handleSignalServiceDataMessage(message);
877 }
878 if (syncMessage.getBlockedList().isPresent()) {
879 System.out.println("Received sync message with block list");
880 System.out.println("Blocked numbers:");
881 final BlockedListMessage blockedList = syncMessage.getBlockedList().get();
882 for (String number : blockedList.getNumbers()) {
883 System.out.println(" - " + number);
884 }
885 }
886 }
887 }
888 } else {
889 System.out.println("Unknown message received.");
890 }
891 System.out.println();
892 }
893
894 private void handleSignalServiceDataMessage(SignalServiceDataMessage message) {
895 System.out.println("Message timestamp: " + formatTimestamp(message.getTimestamp()));
896
897 if (message.getBody().isPresent()) {
898 System.out.println("Body: " + message.getBody().get());
899 }
900 if (message.getGroupInfo().isPresent()) {
901 SignalServiceGroup groupInfo = message.getGroupInfo().get();
902 System.out.println("Group info:");
903 System.out.println(" Id: " + Base64.encodeBytes(groupInfo.getGroupId()));
904 if (groupInfo.getType() == SignalServiceGroup.Type.UPDATE && groupInfo.getName().isPresent()) {
905 System.out.println(" Name: " + groupInfo.getName().get());
906 } else {
907 GroupInfo group = m.getGroup(groupInfo.getGroupId());
908 if (group != null) {
909 System.out.println(" Name: " + group.name);
910 } else {
911 System.out.println(" Name: <Unknown group>");
912 }
913 }
914 System.out.println(" Type: " + groupInfo.getType());
915 if (groupInfo.getMembers().isPresent()) {
916 for (String member : groupInfo.getMembers().get()) {
917 System.out.println(" Member: " + member);
918 }
919 }
920 if (groupInfo.getAvatar().isPresent()) {
921 System.out.println(" Avatar:");
922 printAttachment(groupInfo.getAvatar().get());
923 }
924 }
925 if (message.isEndSession()) {
926 System.out.println("Is end session");
927 }
928 if (message.isExpirationUpdate()) {
929 System.out.println("Is Expiration update: " + message.isExpirationUpdate());
930 }
931 if (message.getExpiresInSeconds() > 0) {
932 System.out.println("Expires in: " + message.getExpiresInSeconds() + " seconds");
933 }
934
935 if (message.getAttachments().isPresent()) {
936 System.out.println("Attachments: ");
937 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
938 printAttachment(attachment);
939 }
940 }
941 }
942
943 private void printAttachment(SignalServiceAttachment attachment) {
944 System.out.println("- " + attachment.getContentType() + " (" + (attachment.isPointer() ? "Pointer" : "") + (attachment.isStream() ? "Stream" : "") + ")");
945 if (attachment.isPointer()) {
946 final SignalServiceAttachmentPointer pointer = attachment.asPointer();
947 System.out.println(" Id: " + pointer.getId() + " Key length: " + pointer.getKey().length + (pointer.getRelay().isPresent() ? " Relay: " + pointer.getRelay().get() : ""));
948 System.out.println(" Size: " + (pointer.getSize().isPresent() ? pointer.getSize().get() + " bytes" : "<unavailable>") + (pointer.getPreview().isPresent() ? " (Preview is available: " + pointer.getPreview().get().length + " bytes)" : ""));
949 File file = m.getAttachmentFile(pointer.getId());
950 if (file.exists()) {
951 System.out.println(" Stored plaintext in: " + file);
952 }
953 }
954 }
955 }
956
957 private static class DbusReceiveMessageHandler extends ReceiveMessageHandler {
958 final DBusConnection conn;
959
960 public DbusReceiveMessageHandler(Manager m, DBusConnection conn) {
961 super(m);
962 this.conn = conn;
963 }
964
965 @Override
966 public void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent content, Throwable exception) {
967 super.handleMessage(envelope, content, exception);
968
969 if (!envelope.isReceipt() && content != null && content.getDataMessage().isPresent()) {
970 SignalServiceDataMessage message = content.getDataMessage().get();
971
972 if (!message.isEndSession() &&
973 !(message.getGroupInfo().isPresent() &&
974 message.getGroupInfo().get().getType() != SignalServiceGroup.Type.DELIVER)) {
975 List<String> attachments = new ArrayList<>();
976 if (message.getAttachments().isPresent()) {
977 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
978 if (attachment.isPointer()) {
979 attachments.add(m.getAttachmentFile(attachment.asPointer().getId()).getAbsolutePath());
980 }
981 }
982 }
983
984 try {
985 conn.sendSignal(new Signal.MessageReceived(
986 SIGNAL_OBJECTPATH,
987 message.getTimestamp(),
988 envelope.getSource(),
989 message.getGroupInfo().isPresent() ? message.getGroupInfo().get().getGroupId() : new byte[0],
990 message.getBody().isPresent() ? message.getBody().get() : "",
991 attachments));
992 } catch (DBusException e) {
993 e.printStackTrace();
994 }
995 }
996 }
997 }
998
999 }
1000
1001 private static String formatTimestamp(long timestamp) {
1002 Date date = new Date(timestamp);
1003 final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); // Quoted "Z" to indicate UTC, no timezone offset
1004 df.setTimeZone(tzUTC);
1005 return timestamp + " (" + df.format(date) + ")";
1006 }
1007 }