]> nmode's Git Repositories - signal-cli/commitdiff
Implement device linking
authorAsamK <asamk@gmx.de>
Thu, 7 Apr 2016 14:30:20 +0000 (16:30 +0200)
committerAsamK <asamk@gmx.de>
Sat, 16 Apr 2016 11:32:21 +0000 (13:32 +0200)
build.gradle
src/main/java/org/asamk/signal/Main.java
src/main/java/org/asamk/signal/Manager.java
src/main/java/org/asamk/signal/UserAlreadyExists.java [new file with mode: 0644]

index 02f8c55bcf51deb6174121c4f27d81da873c4a02..1ee9be22434dc45c4c349118d78719b2f595b48f 100644 (file)
@@ -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'
index e5a2288f1f40b18d8e50b70d036a01dd7c8b60a2..0d2e6f062fc2213a138ef8884cf3b4ab0e9ac9ba 100644 (file)
@@ -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)");
index d626ccaf4c583a33b33453e96ed81a948c0848f0..d442224a854a60680e6f868aab0df0d2da1d225c 100644 (file)
@@ -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<PreKeyRecord> generatePreKeys() {
         List<PreKeyRecord> records = new LinkedList<>();
 
@@ -412,7 +458,7 @@ class Manager implements Signal {
     private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
             throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
         SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
-                signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
+                deviceId, signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
 
         Set<SignalServiceAddress> 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 (file)
index 0000000..2c018ed
--- /dev/null
@@ -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;
+    }
+}