From: AsamK Date: Sat, 21 May 2022 08:08:40 +0000 (+0200) Subject: Decrypt and verify the profile payment address X-Git-Tag: v0.10.7~24 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/bf75d9b4e0385d8b6b86faef8860caa06368c447?hp=3666531f8bfe179bdaf5eaa20ffc2ed16e0fcf9b Decrypt and verify the profile payment address --- diff --git a/graalvm-config-dir/reflect-config.json b/graalvm-config-dir/reflect-config.json index f9deabc6..6d5df0d0 100644 --- a/graalvm-config-dir/reflect-config.json +++ b/graalvm-config-dir/reflect-config.json @@ -538,7 +538,7 @@ {"name":"familyName","parameterTypes":[] }, {"name":"givenName","parameterTypes":[] }, {"name":"lastUpdateTimestamp","parameterTypes":[] }, - {"name":"paymentAddress","parameterTypes":[] } + {"name":"mobileCoinAddress","parameterTypes":[] } ] }, { @@ -2846,6 +2846,21 @@ {"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":[ diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 8a64c8b7..1a876d7e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -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(), diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java index 3f14f645..909337b7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/Profile.java @@ -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 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 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; } } diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 850b6270..0297f6d7 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -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 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 index 00000000..6c34d9c9 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/util/PaymentUtils.java @@ -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}. + *

+ * 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; + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java index 53a9cdb6..e202f70a 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/ProfileUtils.java @@ -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 splitName(String name) { if (name == null) { return new Pair<>(null, null); diff --git a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java index 0bfc7ab9..8fc6f2d1 100644 --- a/src/main/java/org/asamk/signal/commands/ListContactsCommand.java +++ b/src/main/java/org/asamk/signal/commands/ListContactsCommand.java @@ -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 ) {} } }