]> nmode's Git Repositories - signal-cli/blobdiff - src/main/java/org/asamk/signal/Manager.java
Implement device linking
[signal-cli] / src / main / java / org / asamk / signal / Manager.java
index 113fb0cc58838c8aca4ccfd2d9c06e40aa314e38..d442224a854a60680e6f868aab0df0d2da1d225c 100644 (file)
@@ -37,15 +37,21 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager;
 import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
 import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
-import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
+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.SignalServiceSyncMessage;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.push.TrustStore;
+import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
 import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
 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.*;
@@ -69,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;
@@ -92,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());
     }
@@ -118,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")) {
@@ -142,16 +160,21 @@ class Manager implements Signal {
         if (groupStore == null) {
             groupStore = new JsonGroupStore();
         }
-        accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
-        if (accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) {
-            refreshPreKeys();
-            save();
+        accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, deviceId, USER_AGENT);
+        try {
+            if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) {
+                refreshPreKeys();
+                save();
+            }
+        } catch (AuthorizationFailedException e) {
+            System.err.println("Authorization failed, was the number registered elsewhere?");
         }
     }
 
     private void save() {
         ObjectNode rootNode = jsonProcessot.createObjectNode();
         rootNode.put("username", username)
+                .put("deviceId", deviceId)
                 .put("password", password)
                 .put("signalingKey", signalingKey)
                 .put("preKeyIdOffset", preKeyIdOffset)
@@ -194,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<>();
 
@@ -293,7 +346,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));
@@ -309,7 +362,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();
@@ -321,7 +374,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
@@ -374,7 +427,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);
@@ -383,7 +436,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));
@@ -394,7 +447,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();
@@ -403,9 +456,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) {
@@ -419,7 +472,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) {
@@ -448,8 +508,75 @@ class Manager implements Signal {
         void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, GroupInfo group);
     }
 
+    private GroupInfo handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) {
+        GroupInfo group = null;
+        if (message.getGroupInfo().isPresent()) {
+            SignalServiceGroup groupInfo = message.getGroupInfo().get();
+            switch (groupInfo.getType()) {
+                case UPDATE:
+                    try {
+                        group = groupStore.getGroup(groupInfo.getGroupId());
+                    } catch (GroupNotFoundException e) {
+                        group = new GroupInfo(groupInfo.getGroupId());
+                    }
+
+                    if (groupInfo.getAvatar().isPresent()) {
+                        SignalServiceAttachment avatar = groupInfo.getAvatar().get();
+                        if (avatar.isPointer()) {
+                            long avatarId = avatar.asPointer().getId();
+                            try {
+                                retrieveAttachment(avatar.asPointer());
+                                group.avatarId = avatarId;
+                            } catch (IOException | InvalidMessageException e) {
+                                System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
+                            }
+                        }
+                    }
+
+                    if (groupInfo.getName().isPresent()) {
+                        group.name = groupInfo.getName().get();
+                    }
+
+                    if (groupInfo.getMembers().isPresent()) {
+                        group.members.addAll(groupInfo.getMembers().get());
+                    }
+
+                    groupStore.updateGroup(group);
+                    break;
+                case DELIVER:
+                    try {
+                        group = groupStore.getGroup(groupInfo.getGroupId());
+                    } catch (GroupNotFoundException e) {
+                    }
+                    break;
+                case QUIT:
+                    try {
+                        group = groupStore.getGroup(groupInfo.getGroupId());
+                        group.members.remove(source);
+                    } catch (GroupNotFoundException e) {
+                    }
+                    break;
+            }
+        }
+        if (message.isEndSession()) {
+            handleEndSession(isSync ? destination : source);
+        }
+        if (message.getAttachments().isPresent()) {
+            for (SignalServiceAttachment attachment : message.getAttachments().get()) {
+                if (attachment.isPointer()) {
+                    try {
+                        retrieveAttachment(attachment.asPointer());
+                    } catch (IOException | InvalidMessageException e) {
+                        System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
+                    }
+                }
+            }
+        }
+        return group;
+    }
+
     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 {
@@ -466,67 +593,19 @@ class Manager implements Signal {
                         if (content != null) {
                             if (content.getDataMessage().isPresent()) {
                                 SignalServiceDataMessage message = content.getDataMessage().get();
-                                if (message.getGroupInfo().isPresent()) {
-                                    SignalServiceGroup groupInfo = message.getGroupInfo().get();
-                                    switch (groupInfo.getType()) {
-                                        case UPDATE:
-                                            try {
-                                                group = groupStore.getGroup(groupInfo.getGroupId());
-                                            } catch (GroupNotFoundException e) {
-                                                group = new GroupInfo(groupInfo.getGroupId());
-                                            }
-
-                                            if (groupInfo.getAvatar().isPresent()) {
-                                                SignalServiceAttachment avatar = groupInfo.getAvatar().get();
-                                                if (avatar.isPointer()) {
-                                                    long avatarId = avatar.asPointer().getId();
-                                                    try {
-                                                        retrieveAttachment(avatar.asPointer());
-                                                        group.avatarId = avatarId;
-                                                    } catch (IOException | InvalidMessageException e) {
-                                                        System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
-                                                    }
-                                                }
-                                            }
-
-                                            if (groupInfo.getName().isPresent()) {
-                                                group.name = groupInfo.getName().get();
-                                            }
-
-                                            if (groupInfo.getMembers().isPresent()) {
-                                                group.members.addAll(groupInfo.getMembers().get());
-                                            }
-
-                                            groupStore.updateGroup(group);
-                                            break;
-                                        case DELIVER:
-                                            try {
-                                                group = groupStore.getGroup(groupInfo.getGroupId());
-                                            } catch (GroupNotFoundException e) {
-                                            }
-                                            break;
-                                        case QUIT:
-                                            try {
-                                                group = groupStore.getGroup(groupInfo.getGroupId());
-                                                group.members.remove(envelope.getSource());
-                                            } catch (GroupNotFoundException e) {
-                                            }
-                                            break;
-                                    }
+                                group = handleSignalServiceDataMessage(message, false, envelope.getSource(), username);
+                            }
+                            if (content.getSyncMessage().isPresent()) {
+                                SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
+                                if (syncMessage.getSent().isPresent()) {
+                                    SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
+                                    group = handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get());
                                 }
-                                if (message.isEndSession()) {
-                                    handleEndSession(envelope.getSource());
+                                if (syncMessage.getRequest().isPresent()) {
+                                    // TODO
                                 }
-                                if (message.getAttachments().isPresent()) {
-                                    for (SignalServiceAttachment attachment : message.getAttachments().get()) {
-                                        if (attachment.isPointer()) {
-                                            try {
-                                                retrieveAttachment(attachment.asPointer());
-                                            } catch (IOException | InvalidMessageException e) {
-                                                System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
-                                            }
-                                        }
-                                    }
+                                if (syncMessage.getGroups().isPresent()) {
+                                    // TODO
                                 }
                             }
                         }
@@ -551,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);