From 33956bde62d4fc7de5325bf3bb7be3b323863442 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 7 Apr 2016 16:30:20 +0200 Subject: [PATCH] Implement device linking --- build.gradle | 2 +- src/main/java/org/asamk/signal/Main.java | 45 +++++++++++++++- src/main/java/org/asamk/signal/Manager.java | 54 +++++++++++++++++-- .../org/asamk/signal/UserAlreadyExists.java | 19 +++++++ 4 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/asamk/signal/UserAlreadyExists.java diff --git a/build.gradle b/build.gradle index 02f8c55b..1ee9be22 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ repositories { } dependencies { - compile 'org.whispersystems:signal-service-java:2.1.1fetchMessages' + compile 'org.whispersystems:signal-service-java:2.1.1fetchMessages_provisioning' compile 'org.bouncycastle:bcprov-jdk15on:1.54' compile 'commons-io:commons-io:2.4' compile 'net.sourceforge.argparse4j:argparse4j:0.7.0' diff --git a/src/main/java/org/asamk/signal/Main.java b/src/main/java/org/asamk/signal/Main.java index e5a2288f..0d2e6f06 100644 --- a/src/main/java/org/asamk/signal/Main.java +++ b/src/main/java/org/asamk/signal/Main.java @@ -26,6 +26,7 @@ import org.freedesktop.dbus.DBusConnection; import org.freedesktop.dbus.DBusSigHandler; import org.freedesktop.dbus.exceptions.DBusException; import org.freedesktop.dbus.exceptions.DBusExecutionException; +import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.messages.*; import org.whispersystems.signalservice.api.messages.multidevice.ReadMessage; @@ -42,6 +43,7 @@ import java.io.IOException; import java.security.Security; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeoutException; public class Main { @@ -144,6 +146,37 @@ public class Main { System.exit(3); } break; + case "link": + if (dBusConn != null) { + System.err.println("link is not yet implemented via dbus"); + System.exit(1); + } + + // When linking, username is null and we always have to create keys + m.createNewIdentity(); + + String deviceName = ns.getString("name"); + if (deviceName == null) { + deviceName = "cli"; + } + try { + System.out.println(m.getDeviceLinkUri()); + m.finishDeviceLink(deviceName); + System.out.println("Associated with: " + m.getUsername()); + } catch (TimeoutException e) { + System.err.println("Link request timed out, please try again."); + System.exit(3); + } catch (IOException e) { + System.err.println("Link request error: " + e.getMessage()); + System.exit(3); + } catch (InvalidKeyException e) { + e.printStackTrace(); + System.exit(3); + } catch (UserAlreadyExists e) { + System.err.println("The user " + e.getUsername() + " already exists\nDelete \"" + e.getFileName() + "\" before trying again."); + System.exit(3); + } + break; case "send": if (dBusConn == null && !m.isRegistered()) { System.err.println("User is not registered."); @@ -425,6 +458,10 @@ public class Main { .description("valid subcommands") .help("additional help"); + Subparser parserLink = subparsers.addParser("link"); + parserLink.addArgument("-n", "--name") + .help("Specify a name to describe this new device."); + Subparser parserRegister = subparsers.addParser("register"); parserRegister.addArgument("-v", "--voice") .help("The verification should be done over voice, not sms.") @@ -477,7 +514,13 @@ public class Main { try { Namespace ns = parser.parseArgs(args); - if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) { + if ("link".equals(ns.getString("command"))) { + if (ns.getString("username") != null) { + parser.printUsage(); + System.err.println("You cannot specify a username (phone number) when linking"); + System.exit(2); + } + } else if (!ns.getBoolean("dbus") && !ns.getBoolean("dbus_system")) { if (ns.getString("username") == null) { parser.printUsage(); System.err.println("You need to specify a username (phone number)"); diff --git a/src/main/java/org/asamk/signal/Manager.java b/src/main/java/org/asamk/signal/Manager.java index d626ccaf..d442224a 100644 --- a/src/main/java/org/asamk/signal/Manager.java +++ b/src/main/java/org/asamk/signal/Manager.java @@ -49,6 +49,9 @@ import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import java.io.*; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; import java.nio.file.Files; import java.nio.file.Paths; import java.util.*; @@ -72,6 +75,7 @@ class Manager implements Signal { private final ObjectMapper jsonProcessot = new ObjectMapper(); private String username; + int deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID; private String password; private String signalingKey; private int preKeyIdOffset; @@ -95,12 +99,19 @@ class Manager implements Signal { jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); } + public String getUsername() { + return username; + } + public String getFileName() { new File(dataPath).mkdirs(); return dataPath + "/" + username; } public boolean userExists() { + if (username == null) { + return false; + } File f = new File(getFileName()); return !(!f.exists() || f.isDirectory()); } @@ -121,6 +132,10 @@ class Manager implements Signal { public void load() throws IOException, InvalidKeyException { JsonNode rootNode = jsonProcessot.readTree(new File(getFileName())); + JsonNode node = rootNode.get("deviceId"); + if (node != null) { + deviceId = node.asInt(); + } username = getNotNullNode(rootNode, "username").asText(); password = getNotNullNode(rootNode, "password").asText(); if (rootNode.has("signalingKey")) { @@ -145,7 +160,7 @@ class Manager implements Signal { if (groupStore == null) { groupStore = new JsonGroupStore(); } - accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT); try { if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) { refreshPreKeys(); @@ -159,6 +174,7 @@ class Manager implements Signal { private void save() { ObjectNode rootNode = jsonProcessot.createObjectNode(); rootNode.put("username", username) + .put("deviceId", deviceId) .put("password", password) .put("signalingKey", signalingKey) .put("preKeyIdOffset", preKeyIdOffset) @@ -201,6 +217,36 @@ class Manager implements Signal { save(); } + public URI getDeviceLinkUri() throws TimeoutException, IOException { + password = Util.getSecret(18); + + accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT); + String uuid = accountManager.getNewDeviceUuid(); + + registered = false; + try { + return new URI("tsdevice:/?uuid=" + URLEncoder.encode(uuid, "utf-8") + "&pub_key=" + URLEncoder.encode(Base64.encodeBytesWithoutPadding(signalProtocolStore.getIdentityKeyPair().getPublicKey().serialize()), "utf-8")); + } catch (URISyntaxException e) { + // Shouldn't happen + return null; + } + } + + public void finishDeviceLink(String deviceName) throws IOException, InvalidKeyException, TimeoutException, UserAlreadyExists { + signalingKey = Util.getSecret(52); + SignalServiceAccountManager.NewDeviceRegistrationReturn ret = accountManager.finishNewDeviceRegistration(signalProtocolStore.getIdentityKeyPair(), signalingKey, false, true, signalProtocolStore.getLocalRegistrationId(), deviceName); + deviceId = ret.getDeviceId(); + username = ret.getNumber(); + // TODO do this check before actually registering + if (userExists()) { + throw new UserAlreadyExists(username, getFileName()); + } + signalProtocolStore = new JsonSignalProtocolStore(ret.getIdentity(), signalProtocolStore.getLocalRegistrationId()); + + registered = true; + refreshPreKeys(); + } + private List generatePreKeys() { List records = new LinkedList<>(); @@ -412,7 +458,7 @@ class Manager implements Signal { private void sendMessage(SignalServiceDataMessage message, Collection recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException { SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password, - signalProtocolStore, USER_AGENT, Optional.absent()); + deviceId, signalProtocolStore, USER_AGENT, Optional.absent()); Set recipientsTS = new HashSet<>(recipients.size()); for (String recipient : recipients) { @@ -530,7 +576,7 @@ class Manager implements Signal { } public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); SignalServiceMessagePipe messagePipe = null; try { @@ -584,7 +630,7 @@ class Manager implements Signal { } private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException { - final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT); + final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, deviceId, signalingKey, USER_AGENT); File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp"); InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile); diff --git a/src/main/java/org/asamk/signal/UserAlreadyExists.java b/src/main/java/org/asamk/signal/UserAlreadyExists.java new file mode 100644 index 00000000..2c018ed9 --- /dev/null +++ b/src/main/java/org/asamk/signal/UserAlreadyExists.java @@ -0,0 +1,19 @@ +package org.asamk.signal; + +public class UserAlreadyExists extends Exception { + private String username; + private String fileName; + + public UserAlreadyExists(String username, String fileName) { + this.username = username; + this.fileName = fileName; + } + + public String getUsername() { + return username; + } + + public String getFileName() { + return fileName; + } +} -- 2.50.1