]> nmode's Git Repositories - signal-cli/commitdiff
Implement sending messages using unidentified sender
authorAsamK <asamk@gmx.de>
Sun, 22 Mar 2020 16:17:14 +0000 (17:17 +0100)
committerAsamK <asamk@gmx.de>
Sun, 22 Mar 2020 16:17:14 +0000 (17:17 +0100)
src/main/java/org/asamk/signal/ReceiveMessageHandler.java
src/main/java/org/asamk/signal/manager/KeyUtils.java
src/main/java/org/asamk/signal/manager/Manager.java
src/main/java/org/asamk/signal/manager/SignalProfile.java [new file with mode: 0644]

index 34d941ed283e5ab2fe37e0f618438ed88f35c575..bf39b93f6240f2b0b08a8e5f8f98abda5758b605 100644 (file)
@@ -5,6 +5,7 @@ import org.asamk.signal.storage.contacts.ContactInfo;
 import org.asamk.signal.storage.groups.GroupInfo;
 import org.asamk.signal.util.DateUtils;
 import org.asamk.signal.util.Util;
 import org.asamk.signal.storage.groups.GroupInfo;
 import org.asamk.signal.util.DateUtils;
 import org.asamk.signal.util.Util;
+import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
 import org.whispersystems.signalservice.api.messages.SignalServiceContent;
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
 import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
 import org.whispersystems.signalservice.api.messages.SignalServiceContent;
@@ -69,6 +70,11 @@ public class ReceiveMessageHandler implements Manager.ReceiveMessageHandler {
                     System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
                     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");
                     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");
                     System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
                     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");
                     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");
+                } else if (exception instanceof ProtocolUntrustedIdentityException) {
+                    ProtocolUntrustedIdentityException e = (ProtocolUntrustedIdentityException) exception;
+                    System.out.println("The user’s key is untrusted, either the user has reinstalled Signal or a third party sent this message.");
+                    System.out.println("Use 'signal-cli -u " + m.getUsername() + " listIdentities -n " + e.getSender() + "', verify the key and run 'signal-cli -u " + m.getUsername() + " trust -v \"FINGER_PRINT\" " + e.getSender() + "' to mark it as trusted");
+                    System.out.println("If you don't care about security, use 'signal-cli -u " + m.getUsername() + " trust -a " + e.getSender() + "' to trust it without verification");
                 } else {
                     System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")");
                 }
                 } else {
                     System.out.println("Exception: " + exception.getMessage() + " (" + exception.getClass().getSimpleName() + ")");
                 }
index 421a32f4badeeddad6019c2bd3918d4ba7fe8332..364f1eabff87ab813911efb56ba755a95b74596c 100644 (file)
@@ -30,6 +30,10 @@ class KeyUtils {
         return getSecretBytes(16);
     }
 
         return getSecretBytes(16);
     }
 
+    static byte[] createUnrestrictedUnidentifiedAccess() {
+        return getSecretBytes(16);
+    }
+
     private static String getSecret(int size) {
         byte[] secret = getSecretBytes(size);
         return Base64.encodeBytes(secret);
     private static String getSecret(int size) {
         byte[] secret = getSecretBytes(size);
         return Base64.encodeBytes(secret);
index 69e45102832f6e39146ef0849621fceba229f5be..fffa501337bb15e1b97784843cf50d86c8664d23 100644 (file)
@@ -41,7 +41,9 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException;
 import org.signal.libsignal.metadata.ProtocolNoSessionException;
 import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
 import org.signal.libsignal.metadata.SelfSendException;
 import org.signal.libsignal.metadata.ProtocolNoSessionException;
 import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
 import org.signal.libsignal.metadata.SelfSendException;
+import org.signal.libsignal.metadata.certificate.InvalidCertificateException;
 import org.signal.zkgroup.InvalidInputException;
 import org.signal.zkgroup.InvalidInputException;
+import org.signal.zkgroup.VerificationFailedException;
 import org.signal.zkgroup.profiles.ProfileKey;
 import org.whispersystems.libsignal.IdentityKey;
 import org.whispersystems.libsignal.IdentityKeyPair;
 import org.signal.zkgroup.profiles.ProfileKey;
 import org.whispersystems.libsignal.IdentityKey;
 import org.whispersystems.libsignal.IdentityKeyPair;
@@ -61,6 +63,8 @@ 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.SignalServiceMessagePipe;
 import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
+import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
+import org.whispersystems.signalservice.api.crypto.ProfileCipher;
 import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
 import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess;
 import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
@@ -446,6 +450,25 @@ public class Manager implements Signal {
                 account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent());
     }
 
                 account.getDeviceId(), account.getSignalProtocolStore(), BaseConfig.USER_AGENT, account.isMultiDevice(), Optional.fromNullable(messagePipe), Optional.fromNullable(unidentifiedMessagePipe), Optional.absent());
     }
 
+    private SignalServiceProfile getRecipientProfile(SignalServiceAddress address, Optional<UnidentifiedAccess> unidentifiedAccess) throws IOException {
+        SignalServiceMessagePipe pipe = unidentifiedMessagePipe != null && unidentifiedAccess.isPresent() ? unidentifiedMessagePipe
+                : messagePipe;
+
+        if (pipe != null) {
+            try {
+                return pipe.getProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile();
+            } catch (IOException ignored) {
+            }
+        }
+
+        SignalServiceMessageReceiver receiver = getMessageReceiver();
+        try {
+            return receiver.retrieveProfile(address, Optional.absent(), unidentifiedAccess, SignalServiceProfile.RequestType.PROFILE).getProfile();
+        } catch (VerificationFailedException e) {
+            throw new AssertionError(e);
+        }
+    }
+
     private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException {
         File file = getGroupAvatarFile(groupId);
         if (!file.exists()) {
     private Optional<SignalServiceAttachmentStream> createGroupAvatarAttachment(byte[] groupId) throws IOException {
         File file = getGroupAvatarFile(groupId);
         if (!file.exists()) {
@@ -874,31 +897,98 @@ public class Manager implements Signal {
         }
     }
 
         }
     }
 
+    private byte[] getSenderCertificate() throws IOException {
+        byte[] certificate = accountManager.getSenderCertificate();
+        // TODO cache for a day
+        return certificate;
+    }
+
     private byte[] getSelfUnidentifiedAccessKey() {
         return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
     }
 
     private byte[] getSelfUnidentifiedAccessKey() {
         return UnidentifiedAccess.deriveAccessKeyFrom(account.getProfileKey());
     }
 
-    private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) {
-        // TODO implement
-        return null;
+    private static SignalProfile decryptProfile(SignalServiceProfile encryptedProfile, ProfileKey profileKey) throws IOException {
+        ProfileCipher profileCipher = new ProfileCipher(profileKey);
+        try {
+            return new SignalProfile(
+                    encryptedProfile.getIdentityKey(),
+                    encryptedProfile.getName() == null ? null : new String(profileCipher.decryptName(Base64.decode(encryptedProfile.getName()))),
+                    encryptedProfile.getAvatar(),
+                    encryptedProfile.getUnidentifiedAccess() == null || !profileCipher.verifyUnidentifiedAccess(Base64.decode(encryptedProfile.getUnidentifiedAccess())) ? null : encryptedProfile.getUnidentifiedAccess(),
+                    encryptedProfile.isUnrestrictedUnidentifiedAccess()
+            );
+        } catch (InvalidCiphertextException e) {
+            return null;
+        }
     }
 
     }
 
-    private Optional<UnidentifiedAccessPair> getAccessForSync() {
-        // TODO implement
-        return Optional.absent();
+    private byte[] getTargetUnidentifiedAccessKey(SignalServiceAddress recipient) throws IOException {
+        ContactInfo contact = account.getContactStore().getContact(recipient.getNumber().get());
+        if (contact == null || contact.profileKey == null) {
+            return null;
+        }
+        ProfileKey theirProfileKey;
+        try {
+            theirProfileKey = new ProfileKey(Base64.decode(contact.profileKey));
+        } catch (InvalidInputException e) {
+            throw new AssertionError(e);
+        }
+        SignalProfile targetProfile = decryptProfile(getRecipientProfile(recipient, Optional.absent()), theirProfileKey);
+
+        if (targetProfile == null || targetProfile.getUnidentifiedAccess() == null) {
+            return null;
+        }
+
+        if (targetProfile.isUnrestrictedUnidentifiedAccess()) {
+            return KeyUtils.createUnrestrictedUnidentifiedAccess();
+        }
+
+        return UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey);
+    }
+
+    private Optional<UnidentifiedAccessPair> getAccessForSync() throws IOException {
+        byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
+        byte[] selfUnidentifiedAccessCertificate = getSenderCertificate();
+
+        if (selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
+            return Optional.absent();
+        }
+
+        try {
+            return Optional.of(new UnidentifiedAccessPair(
+                    new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
+                    new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
+            ));
+        } catch (InvalidCertificateException e) {
+            return Optional.absent();
+        }
     }
 
     }
 
-    private List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<SignalServiceAddress> recipients) {
+    private List<Optional<UnidentifiedAccessPair>> getAccessFor(Collection<SignalServiceAddress> recipients) throws IOException {
         List<Optional<UnidentifiedAccessPair>> result = new ArrayList<>(recipients.size());
         for (SignalServiceAddress recipient : recipients) {
         List<Optional<UnidentifiedAccessPair>> result = new ArrayList<>(recipients.size());
         for (SignalServiceAddress recipient : recipients) {
-            result.add(Optional.absent());
+            result.add(getAccessFor(recipient));
         }
         return result;
     }
 
         }
         return result;
     }
 
-    private Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress recipient) {
-        // TODO implement
-        return Optional.absent();
+    private Optional<UnidentifiedAccessPair> getAccessFor(SignalServiceAddress recipient) throws IOException {
+        byte[] recipientUnidentifiedAccessKey = getTargetUnidentifiedAccessKey(recipient);
+        byte[] selfUnidentifiedAccessKey = getSelfUnidentifiedAccessKey();
+        byte[] selfUnidentifiedAccessCertificate = getSenderCertificate();
+
+        if (recipientUnidentifiedAccessKey == null || selfUnidentifiedAccessKey == null || selfUnidentifiedAccessCertificate == null) {
+            return Optional.absent();
+        }
+
+        try {
+            return Optional.of(new UnidentifiedAccessPair(
+                    new UnidentifiedAccess(recipientUnidentifiedAccessKey, selfUnidentifiedAccessCertificate),
+                    new UnidentifiedAccess(selfUnidentifiedAccessKey, selfUnidentifiedAccessCertificate)
+            ));
+        } catch (InvalidCertificateException e) {
+            return Optional.absent();
+        }
     }
 
     private void sendSyncMessage(SignalServiceSyncMessage message)
     }
 
     private void sendSyncMessage(SignalServiceSyncMessage message)
@@ -945,6 +1035,12 @@ public class Manager implements Signal {
             return Collections.emptyList();
         }
 
             return Collections.emptyList();
         }
 
+        if (messagePipe == null) {
+            messagePipe = getMessageReceiver().createMessagePipe();
+        }
+        if (unidentifiedMessagePipe == null) {
+            unidentifiedMessagePipe = getMessageReceiver().createUnidentifiedMessagePipe();
+        }
         SignalServiceDataMessage message = null;
         try {
             SignalServiceMessageSender messageSender = getMessageSender();
         SignalServiceDataMessage message = null;
         try {
             SignalServiceMessageSender messageSender = getMessageSender();
diff --git a/src/main/java/org/asamk/signal/manager/SignalProfile.java b/src/main/java/org/asamk/signal/manager/SignalProfile.java
new file mode 100644 (file)
index 0000000..4f529c0
--- /dev/null
@@ -0,0 +1,53 @@
+package org.asamk.signal.manager;
+
+public class SignalProfile {
+
+    private final String identityKey;
+
+    private final String name;
+
+    private final String avatar;
+
+    private final String unidentifiedAccess;
+
+    private final boolean unrestrictedUnidentifiedAccess;
+
+    public SignalProfile(final String identityKey, final String name, final String avatar, final String unidentifiedAccess, final boolean unrestrictedUnidentifiedAccess) {
+        this.identityKey = identityKey;
+        this.name = name;
+        this.avatar = avatar;
+        this.unidentifiedAccess = unidentifiedAccess;
+        this.unrestrictedUnidentifiedAccess = unrestrictedUnidentifiedAccess;
+    }
+
+    public String getIdentityKey() {
+        return identityKey;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getAvatar() {
+        return avatar;
+    }
+
+    public String getUnidentifiedAccess() {
+        return unidentifiedAccess;
+    }
+
+    public boolean isUnrestrictedUnidentifiedAccess() {
+        return unrestrictedUnidentifiedAccess;
+    }
+
+    @Override
+    public String toString() {
+        return "SignalProfile{" +
+                "identityKey='" + identityKey + '\'' +
+                ", name='" + name + '\'' +
+                ", avatar='" + avatar + '\'' +
+                ", unidentifiedAccess='" + unidentifiedAccess + '\'' +
+                ", unrestrictedUnidentifiedAccess=" + unrestrictedUnidentifiedAccess +
+                '}';
+    }
+}