]> nmode's Git Repositories - signal-cli/blobdiff - src/main/java/org/asamk/signal/Manager.java
Add possiblity to add new device, as master
[signal-cli] / src / main / java / org / asamk / signal / Manager.java
index 2fc54504ad4121ad450346a7ae08a33dc030eec9..eb883bb84286aba77b7e7590d08884d3c097d281 100644 (file)
@@ -23,10 +23,12 @@ import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.SerializationFeature;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.apache.http.util.TextUtils;
 import org.asamk.Signal;
 import org.whispersystems.libsignal.*;
 import org.whispersystems.libsignal.ecc.Curve;
 import org.whispersystems.libsignal.ecc.ECKeyPair;
+import org.whispersystems.libsignal.ecc.ECPublicKey;
 import org.whispersystems.libsignal.state.PreKeyRecord;
 import org.whispersystems.libsignal.state.SignalProtocolStore;
 import org.whispersystems.libsignal.state.SignedPreKeyRecord;
@@ -40,6 +42,7 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
 import org.whispersystems.signalservice.api.crypto.*;
 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
 import org.whispersystems.signalservice.api.messages.*;
+import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage;
 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.push.TrustStore;
@@ -47,8 +50,13 @@ import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedE
 import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
 import org.whispersystems.signalservice.api.util.InvalidNumberException;
 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
+import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
 
 import java.io.*;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.*;
@@ -72,6 +80,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 +104,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 +137,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 +165,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 +179,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 +222,80 @@ 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();
+        save();
+    }
+
+
+    public static Map<String, String> getQueryMap(String query) {
+        String[] params = query.split("&");
+        Map<String, String> map = new HashMap<>();
+        for (String param : params) {
+            String name = null;
+            try {
+                name = URLDecoder.decode(param.split("=")[0], "utf-8");
+            } catch (UnsupportedEncodingException e) {
+                // Impossible
+            }
+            String value = null;
+            try {
+                value = URLDecoder.decode(param.split("=")[1], "utf-8");
+            } catch (UnsupportedEncodingException e) {
+                // Impossible
+            }
+            map.put(name, value);
+        }
+        return map;
+    }
+
+    public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException {
+        Map<String, String> query = getQueryMap(linkUri.getQuery());
+        String deviceIdentifier = query.get("uuid");
+        String publicKeyEncoded = query.get("pub_key");
+
+        if (TextUtils.isEmpty(deviceIdentifier) || TextUtils.isEmpty(publicKeyEncoded)) {
+            throw new RuntimeException("Invalid device link uri");
+        }
+
+        ECPublicKey deviceKey = Curve.decodePoint(Base64.decode(publicKeyEncoded), 0);
+
+        addDeviceLink(deviceIdentifier, deviceKey);
+    }
+
+    private void addDeviceLink(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException {
+        IdentityKeyPair identityKeyPair = signalProtocolStore.getIdentityKeyPair();
+        String verificationCode = accountManager.getNewDeviceVerificationCode();
+
+        accountManager.addDevice(deviceIdentifier, deviceKey, identityKeyPair, verificationCode);
+    }
+
     private List<PreKeyRecord> generatePreKeys() {
         List<PreKeyRecord> records = new LinkedList<>();
 
@@ -300,7 +395,7 @@ class Manager implements Signal {
     @Override
     public void sendGroupMessage(String messageText, List<String> attachments,
                                  byte[] groupId)
-            throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+            throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException {
         final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
         if (attachments != null) {
             messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
@@ -316,7 +411,7 @@ class Manager implements Signal {
         sendMessage(message, groupStore.getGroup(groupId).members);
     }
 
-    public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions {
+    public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException {
         SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
                 .withId(groupId)
                 .build();
@@ -328,7 +423,7 @@ class Manager implements Signal {
         sendMessage(message, groupStore.getGroup(groupId).members);
     }
 
-    public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException {
+    public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException {
         GroupInfo g;
         if (groupId == null) {
             // Create new group
@@ -381,7 +476,7 @@ class Manager implements Signal {
 
     @Override
     public void sendMessage(String message, List<String> attachments, String recipient)
-            throws EncapsulatedExceptions, AttachmentInvalidException, IOException {
+            throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException {
         List<String> recipients = new ArrayList<>(1);
         recipients.add(recipient);
         sendMessage(message, attachments, recipients);
@@ -390,7 +485,7 @@ class Manager implements Signal {
     @Override
     public void sendMessage(String messageText, List<String> attachments,
                             List<String> recipients)
-            throws IOException, EncapsulatedExceptions, AttachmentInvalidException {
+            throws IOException, EncapsulatedExceptions, AttachmentInvalidException, UntrustedIdentityException {
         final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
         if (attachments != null) {
             messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
@@ -401,7 +496,7 @@ class Manager implements Signal {
     }
 
     @Override
-    public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions {
+    public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
         SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
                 .asEndSessionMessage()
                 .build();
@@ -410,9 +505,9 @@ class Manager implements Signal {
     }
 
     private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
-            throws IOException, EncapsulatedExceptions {
+            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) {
@@ -426,7 +521,14 @@ class Manager implements Signal {
             }
         }
 
-        messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
+        if (message.getGroupInfo().isPresent()) {
+            messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
+        } else {
+            // Send to all individually, so sync messages are sent correctly
+            for (SignalServiceAddress address : recipientsTS) {
+                messageSender.sendMessage(address, message);
+            }
+        }
 
         if (message.isEndSession()) {
             for (SignalServiceAddress recipient : recipientsTS) {
@@ -523,7 +625,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 {
@@ -577,7 +679,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);