]> nmode's Git Repositories - signal-cli/commitdiff
Decrypt and verify the profile payment address
authorAsamK <asamk@gmx.de>
Sat, 21 May 2022 08:08:40 +0000 (10:08 +0200)
committerAsamK <asamk@gmx.de>
Sat, 21 May 2022 08:44:28 +0000 (10:44 +0200)
graalvm-config-dir/reflect-config.json
lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java
lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java
lib/src/main/java/org/asamk/signal/manager/util/PaymentUtils.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java
src/main/java/org/asamk/signal/commands/ListContactsCommand.java

index f9deabc65e2feed60d74092cd071d4d0f8a75312..6d5df0d0cec9b8b84fe92be4f163d7bc468c2bbd 100644 (file)
     {"name":"familyName","parameterTypes":[] }, 
     {"name":"givenName","parameterTypes":[] }, 
     {"name":"lastUpdateTimestamp","parameterTypes":[] }, 
-    {"name":"paymentAddress","parameterTypes":[] }
+    {"name":"mobileCoinAddress","parameterTypes":[] }
   ]
 },
 {
     {"name":"padding_"}
   ]
 },
+{
+  "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$PaymentAddress",
+  "fields":[
+    {"name":"addressCase_"}, 
+    {"name":"address_"}
+  ]
+},
+{
+  "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$PaymentAddress$MobileCoinAddress",
+  "fields":[
+    {"name":"address_"}, 
+    {"name":"bitField0_"}, 
+    {"name":"signature_"}
+  ]
+},
 {
   "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Preview",
   "fields":[
index 8a64c8b7ec04530c86d80a66f19841c46334be46..1a876d7ec4d1826b52bc0ff760f94d2fc5615483 100644 (file)
@@ -1,7 +1,5 @@
 package org.asamk.signal.manager.helper;
 
-import com.google.protobuf.InvalidProtocolBufferException;
-
 import org.asamk.signal.manager.SignalDependencies;
 import org.asamk.signal.manager.config.ServiceConfig;
 import org.asamk.signal.manager.groups.GroupNotFoundException;
@@ -13,6 +11,7 @@ import org.asamk.signal.manager.storage.recipients.RecipientAddress;
 import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.asamk.signal.manager.util.IOUtils;
 import org.asamk.signal.manager.util.KeyUtils;
+import org.asamk.signal.manager.util.PaymentUtils;
 import org.asamk.signal.manager.util.ProfileUtils;
 import org.asamk.signal.manager.util.Utils;
 import org.signal.libsignal.protocol.IdentityKey;
@@ -29,7 +28,6 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
 import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
 import org.whispersystems.signalservice.api.services.ProfileService;
-import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
 
 import java.io.File;
 import java.io.IOException;
@@ -185,13 +183,9 @@ public final class ProfileHelper {
                 final var avatarUploadParams = streamDetails != null
                         ? AvatarUploadParams.forAvatar(streamDetails)
                         : avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false);
-                final var paymentsAddress = Optional.ofNullable(newProfile.getPaymentAddress()).map(data -> {
-                    try {
-                        return SignalServiceProtos.PaymentAddress.parseFrom(data);
-                    } catch (InvalidProtocolBufferException e) {
-                        return null;
-                    }
-                });
+                final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress())
+                        .map(address -> PaymentUtils.signPaymentsAddress(address,
+                                account.getAciIdentityKeyPair().getPrivateKey()));
                 logger.debug("Uploading new profile");
                 final var avatarPath = dependencies.getAccountManager()
                         .setVersionedProfile(account.getAci(),
index 3f14f645fb35a8eeef97bbdc114ccc1867f06c4e..909337b7c688d1908a552b6ef574158891643248 100644 (file)
@@ -20,7 +20,7 @@ public class Profile {
 
     private final String avatarUrlPath;
 
-    private final byte[] paymentAddress;
+    private final byte[] mobileCoinAddress;
 
     private final UnidentifiedAccessMode unidentifiedAccessMode;
 
@@ -33,7 +33,7 @@ public class Profile {
             final String about,
             final String aboutEmoji,
             final String avatarUrlPath,
-            final byte[] paymentAddress,
+            final byte[] mobileCoinAddress,
             final UnidentifiedAccessMode unidentifiedAccessMode,
             final Set<Capability> capabilities
     ) {
@@ -43,7 +43,7 @@ public class Profile {
         this.about = about;
         this.aboutEmoji = aboutEmoji;
         this.avatarUrlPath = avatarUrlPath;
-        this.paymentAddress = paymentAddress;
+        this.mobileCoinAddress = mobileCoinAddress;
         this.unidentifiedAccessMode = unidentifiedAccessMode;
         this.capabilities = capabilities;
     }
@@ -55,7 +55,7 @@ public class Profile {
         about = builder.about;
         aboutEmoji = builder.aboutEmoji;
         avatarUrlPath = builder.avatarUrlPath;
-        paymentAddress = builder.paymentAddress;
+        mobileCoinAddress = builder.mobileCoinAddress;
         unidentifiedAccessMode = builder.unidentifiedAccessMode;
         capabilities = builder.capabilities;
     }
@@ -72,7 +72,7 @@ public class Profile {
         builder.about = copy.getAbout();
         builder.aboutEmoji = copy.getAboutEmoji();
         builder.avatarUrlPath = copy.getAvatarUrlPath();
-        builder.paymentAddress = copy.getPaymentAddress();
+        builder.mobileCoinAddress = copy.getMobileCoinAddress();
         builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode();
         builder.capabilities = copy.getCapabilities();
         return builder;
@@ -124,8 +124,8 @@ public class Profile {
         return avatarUrlPath;
     }
 
-    public byte[] getPaymentAddress() {
-        return paymentAddress;
+    public byte[] getMobileCoinAddress() {
+        return mobileCoinAddress;
     }
 
     public UnidentifiedAccessMode getUnidentifiedAccessMode() {
@@ -200,7 +200,7 @@ public class Profile {
         private String about;
         private String aboutEmoji;
         private String avatarUrlPath;
-        private byte[] paymentAddress;
+        private byte[] mobileCoinAddress;
         private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
         private Set<Capability> capabilities = Collections.emptySet();
         private long lastUpdateTimestamp = 0;
@@ -252,8 +252,8 @@ public class Profile {
             return this;
         }
 
-        public Builder withPaymentAddress(final byte[] val) {
-            paymentAddress = val;
+        public Builder withMobileCoinAddress(final byte[] val) {
+            mobileCoinAddress = val;
             return this;
         }
     }
index 850b62703df88871f25a97b7a429dc8349767fde..0297f6d7d290b93ae29529f14a0a1a95e1ee6c67 100644 (file)
@@ -105,9 +105,9 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
                             r.profile.about,
                             r.profile.aboutEmoji,
                             r.profile.avatarUrlPath,
-                            r.profile.paymentAddress == null
+                            r.profile.mobileCoinAddress == null
                                     ? null
-                                    : Base64.getDecoder().decode(r.profile.paymentAddress),
+                                    : Base64.getDecoder().decode(r.profile.mobileCoinAddress),
                             Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode),
                             r.profile.capabilities.stream()
                                     .map(Profile.Capability::valueOfOrNull)
@@ -592,9 +592,9 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
                             recipientProfile.getAbout(),
                             recipientProfile.getAboutEmoji(),
                             recipientProfile.getAvatarUrlPath(),
-                            recipientProfile.getPaymentAddress() == null
+                            recipientProfile.getMobileCoinAddress() == null
                                     ? null
-                                    : base64.encodeToString(recipientProfile.getPaymentAddress()),
+                                    : base64.encodeToString(recipientProfile.getMobileCoinAddress()),
                             recipientProfile.getUnidentifiedAccessMode().name(),
                             recipientProfile.getCapabilities().stream().map(Enum::name).collect(Collectors.toSet()));
             return new Storage.Recipient(pair.getKey().id(),
@@ -651,7 +651,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
                     String about,
                     String aboutEmoji,
                     String avatarUrlPath,
-                    String paymentAddress,
+                    String mobileCoinAddress,
                     String unidentifiedAccessMode,
                     Set<String> capabilities
             ) {}
diff --git a/lib/src/main/java/org/asamk/signal/manager/util/PaymentUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/PaymentUtils.java
new file mode 100644 (file)
index 0000000..6c34d9c
--- /dev/null
@@ -0,0 +1,52 @@
+package org.asamk.signal.manager.util;
+
+import com.google.protobuf.ByteString;
+
+import org.signal.libsignal.protocol.IdentityKey;
+import org.signal.libsignal.protocol.IdentityKeyPair;
+import org.signal.libsignal.protocol.ecc.ECPrivateKey;
+import org.signal.libsignal.protocol.ecc.ECPublicKey;
+import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
+
+public class PaymentUtils {
+
+    private PaymentUtils() {
+    }
+
+    /**
+     * Signs the supplied address bytes with the {@link IdentityKeyPair}'s private key and returns a proto that includes it, and it's signature.
+     */
+    public static SignalServiceProtos.PaymentAddress signPaymentsAddress(
+            byte[] publicAddressBytes, ECPrivateKey privateKey
+    ) {
+        byte[] signature = privateKey.calculateSignature(publicAddressBytes);
+
+        return SignalServiceProtos.PaymentAddress.newBuilder()
+                .setMobileCoinAddress(SignalServiceProtos.PaymentAddress.MobileCoinAddress.newBuilder()
+                        .setAddress(ByteString.copyFrom(publicAddressBytes))
+                        .setSignature(ByteString.copyFrom(signature)))
+                .build();
+    }
+
+    /**
+     * Verifies that the payments address is signed with the supplied {@link IdentityKey}.
+     * <p>
+     * Returns the validated bytes if so, otherwise returns null.
+     */
+    public static byte[] verifyPaymentsAddress(
+            SignalServiceProtos.PaymentAddress paymentAddress, ECPublicKey publicKey
+    ) {
+        if (!paymentAddress.hasMobileCoinAddress()) {
+            return null;
+        }
+
+        byte[] bytes = paymentAddress.getMobileCoinAddress().getAddress().toByteArray();
+        byte[] signature = paymentAddress.getMobileCoinAddress().getSignature().toByteArray();
+
+        if (signature.length != 64 || !publicKey.verifySignature(bytes, signature)) {
+            return null;
+        }
+
+        return bytes;
+    }
+}
index 53a9cdb62db5b396c3d2a1c2674d4be6e56c2f70..e202f70af56f227d6f7b6bf61ce93af5cb14238b 100644 (file)
@@ -1,14 +1,21 @@
 package org.asamk.signal.manager.util;
 
+import com.google.protobuf.InvalidProtocolBufferException;
+
 import org.asamk.signal.manager.api.Pair;
 import org.asamk.signal.manager.storage.recipients.Profile;
+import org.signal.libsignal.protocol.IdentityKey;
+import org.signal.libsignal.protocol.InvalidKeyException;
+import org.signal.libsignal.protocol.ecc.ECPublicKey;
 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
 import org.whispersystems.signalservice.api.crypto.ProfileCipher;
 import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
+import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
 
+import java.io.IOException;
 import java.util.Base64;
 import java.util.HashSet;
 
@@ -20,6 +27,12 @@ public class ProfileUtils {
             final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
     ) {
         var profileCipher = new ProfileCipher(profileKey);
+        IdentityKey identityKey = null;
+        try {
+            identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()), 0);
+        } catch (InvalidKeyException ignored) {
+        }
+
         try {
             var name = decrypt(encryptedProfile.getName(), profileCipher);
             var about = trimZeros(decrypt(encryptedProfile.getAbout(), profileCipher));
@@ -32,7 +45,11 @@ public class ProfileUtils {
                     about,
                     aboutEmoji,
                     encryptedProfile.getAvatar(),
-                    encryptedProfile.getPaymentAddress(),
+                    identityKey == null || encryptedProfile.getPaymentAddress() == null
+                            ? null
+                            : decryptAndVerifyMobileCoinAddress(encryptedProfile.getPaymentAddress(),
+                                    profileCipher,
+                                    identityKey.getPublicKey()),
                     getUnidentifiedAccessMode(encryptedProfile, profileCipher),
                     getCapabilities(encryptedProfile));
         } catch (InvalidCiphertextException e) {
@@ -88,6 +105,26 @@ public class ProfileUtils {
         }
     }
 
+    private static byte[] decryptAndVerifyMobileCoinAddress(
+            final byte[] encryptedPaymentAddress, final ProfileCipher profileCipher, final ECPublicKey publicKey
+    ) throws InvalidCiphertextException {
+        byte[] decrypted;
+        try {
+            decrypted = profileCipher.decryptWithLength(encryptedPaymentAddress);
+        } catch (IOException e) {
+            return null;
+        }
+
+        SignalServiceProtos.PaymentAddress paymentAddress;
+        try {
+            paymentAddress = SignalServiceProtos.PaymentAddress.parseFrom(decrypted);
+        } catch (InvalidProtocolBufferException e) {
+            return null;
+        }
+
+        return PaymentUtils.verifyPaymentsAddress(paymentAddress, publicKey);
+    }
+
     private static Pair<String, String> splitName(String name) {
         if (name == null) {
             return new Pair<>(null, null);
index 0bfc7ab9a377c99134dd4a8c2e1d831112c896a1..8fc6f2d1fd944f060b50dc314023155e6fe3b2b1 100644 (file)
@@ -81,10 +81,10 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
                                         r.getProfile().getFamilyName(),
                                         r.getProfile().getAbout(),
                                         r.getProfile().getAboutEmoji(),
-                                        r.getProfile().getPaymentAddress() == null
+                                        r.getProfile().getMobileCoinAddress() == null
                                                 ? null
                                                 : Base64.getEncoder()
-                                                        .encodeToString(r.getProfile().getPaymentAddress())));
+                                                        .encodeToString(r.getProfile().getMobileCoinAddress())));
             }).toList();
 
             writer.write(jsonContacts);
@@ -92,12 +92,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
     }
 
     private record JsonContact(
-            String number,
-            String uuid,
-            String name,
-            boolean isBlocked,
-            int messageExpirationTime,
-            JsonProfile profile
+            String number, String uuid, String name, boolean isBlocked, int messageExpirationTime, JsonProfile profile
     ) {
 
         private record JsonProfile(
@@ -106,7 +101,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
                 String familyName,
                 String about,
                 String aboutEmoji,
-                String paymentAddress
+                String mobileCoinAddress
         ) {}
     }
 }