Decrypt and verify the profile payment address

This commit is contained in:
AsamK 2022-05-21 10:08:40 +02:00
parent 3666531f8b
commit bf75d9b4e0
7 changed files with 129 additions and 36 deletions

View file

@ -538,7 +538,7 @@
{"name":"familyName","parameterTypes":[] }, {"name":"familyName","parameterTypes":[] },
{"name":"givenName","parameterTypes":[] }, {"name":"givenName","parameterTypes":[] },
{"name":"lastUpdateTimestamp","parameterTypes":[] }, {"name":"lastUpdateTimestamp","parameterTypes":[] },
{"name":"paymentAddress","parameterTypes":[] } {"name":"mobileCoinAddress","parameterTypes":[] }
] ]
}, },
{ {
@ -2846,6 +2846,21 @@
{"name":"padding_"} {"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", "name":"org.whispersystems.signalservice.internal.push.SignalServiceProtos$Preview",
"fields":[ "fields":[

View file

@ -1,7 +1,5 @@
package org.asamk.signal.manager.helper; package org.asamk.signal.manager.helper;
import com.google.protobuf.InvalidProtocolBufferException;
import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.SignalDependencies;
import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceConfig;
import org.asamk.signal.manager.groups.GroupNotFoundException; 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.storage.recipients.RecipientId;
import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.IOUtils;
import org.asamk.signal.manager.util.KeyUtils; 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.ProfileUtils;
import org.asamk.signal.manager.util.Utils; import org.asamk.signal.manager.util.Utils;
import org.signal.libsignal.protocol.IdentityKey; 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.NotFoundException;
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException; import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.services.ProfileService; import org.whispersystems.signalservice.api.services.ProfileService;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -185,13 +183,9 @@ public final class ProfileHelper {
final var avatarUploadParams = streamDetails != null final var avatarUploadParams = streamDetails != null
? AvatarUploadParams.forAvatar(streamDetails) ? AvatarUploadParams.forAvatar(streamDetails)
: avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false); : avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false);
final var paymentsAddress = Optional.ofNullable(newProfile.getPaymentAddress()).map(data -> { final var paymentsAddress = Optional.ofNullable(newProfile.getMobileCoinAddress())
try { .map(address -> PaymentUtils.signPaymentsAddress(address,
return SignalServiceProtos.PaymentAddress.parseFrom(data); account.getAciIdentityKeyPair().getPrivateKey()));
} catch (InvalidProtocolBufferException e) {
return null;
}
});
logger.debug("Uploading new profile"); logger.debug("Uploading new profile");
final var avatarPath = dependencies.getAccountManager() final var avatarPath = dependencies.getAccountManager()
.setVersionedProfile(account.getAci(), .setVersionedProfile(account.getAci(),

View file

@ -20,7 +20,7 @@ public class Profile {
private final String avatarUrlPath; private final String avatarUrlPath;
private final byte[] paymentAddress; private final byte[] mobileCoinAddress;
private final UnidentifiedAccessMode unidentifiedAccessMode; private final UnidentifiedAccessMode unidentifiedAccessMode;
@ -33,7 +33,7 @@ public class Profile {
final String about, final String about,
final String aboutEmoji, final String aboutEmoji,
final String avatarUrlPath, final String avatarUrlPath,
final byte[] paymentAddress, final byte[] mobileCoinAddress,
final UnidentifiedAccessMode unidentifiedAccessMode, final UnidentifiedAccessMode unidentifiedAccessMode,
final Set<Capability> capabilities final Set<Capability> capabilities
) { ) {
@ -43,7 +43,7 @@ public class Profile {
this.about = about; this.about = about;
this.aboutEmoji = aboutEmoji; this.aboutEmoji = aboutEmoji;
this.avatarUrlPath = avatarUrlPath; this.avatarUrlPath = avatarUrlPath;
this.paymentAddress = paymentAddress; this.mobileCoinAddress = mobileCoinAddress;
this.unidentifiedAccessMode = unidentifiedAccessMode; this.unidentifiedAccessMode = unidentifiedAccessMode;
this.capabilities = capabilities; this.capabilities = capabilities;
} }
@ -55,7 +55,7 @@ public class Profile {
about = builder.about; about = builder.about;
aboutEmoji = builder.aboutEmoji; aboutEmoji = builder.aboutEmoji;
avatarUrlPath = builder.avatarUrlPath; avatarUrlPath = builder.avatarUrlPath;
paymentAddress = builder.paymentAddress; mobileCoinAddress = builder.mobileCoinAddress;
unidentifiedAccessMode = builder.unidentifiedAccessMode; unidentifiedAccessMode = builder.unidentifiedAccessMode;
capabilities = builder.capabilities; capabilities = builder.capabilities;
} }
@ -72,7 +72,7 @@ public class Profile {
builder.about = copy.getAbout(); builder.about = copy.getAbout();
builder.aboutEmoji = copy.getAboutEmoji(); builder.aboutEmoji = copy.getAboutEmoji();
builder.avatarUrlPath = copy.getAvatarUrlPath(); builder.avatarUrlPath = copy.getAvatarUrlPath();
builder.paymentAddress = copy.getPaymentAddress(); builder.mobileCoinAddress = copy.getMobileCoinAddress();
builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode(); builder.unidentifiedAccessMode = copy.getUnidentifiedAccessMode();
builder.capabilities = copy.getCapabilities(); builder.capabilities = copy.getCapabilities();
return builder; return builder;
@ -124,8 +124,8 @@ public class Profile {
return avatarUrlPath; return avatarUrlPath;
} }
public byte[] getPaymentAddress() { public byte[] getMobileCoinAddress() {
return paymentAddress; return mobileCoinAddress;
} }
public UnidentifiedAccessMode getUnidentifiedAccessMode() { public UnidentifiedAccessMode getUnidentifiedAccessMode() {
@ -200,7 +200,7 @@ public class Profile {
private String about; private String about;
private String aboutEmoji; private String aboutEmoji;
private String avatarUrlPath; private String avatarUrlPath;
private byte[] paymentAddress; private byte[] mobileCoinAddress;
private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN; private UnidentifiedAccessMode unidentifiedAccessMode = UnidentifiedAccessMode.UNKNOWN;
private Set<Capability> capabilities = Collections.emptySet(); private Set<Capability> capabilities = Collections.emptySet();
private long lastUpdateTimestamp = 0; private long lastUpdateTimestamp = 0;
@ -252,8 +252,8 @@ public class Profile {
return this; return this;
} }
public Builder withPaymentAddress(final byte[] val) { public Builder withMobileCoinAddress(final byte[] val) {
paymentAddress = val; mobileCoinAddress = val;
return this; return this;
} }
} }

View file

@ -105,9 +105,9 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
r.profile.about, r.profile.about,
r.profile.aboutEmoji, r.profile.aboutEmoji,
r.profile.avatarUrlPath, r.profile.avatarUrlPath,
r.profile.paymentAddress == null r.profile.mobileCoinAddress == null
? null ? null
: Base64.getDecoder().decode(r.profile.paymentAddress), : Base64.getDecoder().decode(r.profile.mobileCoinAddress),
Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode), Profile.UnidentifiedAccessMode.valueOfOrUnknown(r.profile.unidentifiedAccessMode),
r.profile.capabilities.stream() r.profile.capabilities.stream()
.map(Profile.Capability::valueOfOrNull) .map(Profile.Capability::valueOfOrNull)
@ -592,9 +592,9 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
recipientProfile.getAbout(), recipientProfile.getAbout(),
recipientProfile.getAboutEmoji(), recipientProfile.getAboutEmoji(),
recipientProfile.getAvatarUrlPath(), recipientProfile.getAvatarUrlPath(),
recipientProfile.getPaymentAddress() == null recipientProfile.getMobileCoinAddress() == null
? null ? null
: base64.encodeToString(recipientProfile.getPaymentAddress()), : base64.encodeToString(recipientProfile.getMobileCoinAddress()),
recipientProfile.getUnidentifiedAccessMode().name(), recipientProfile.getUnidentifiedAccessMode().name(),
recipientProfile.getCapabilities().stream().map(Enum::name).collect(Collectors.toSet())); recipientProfile.getCapabilities().stream().map(Enum::name).collect(Collectors.toSet()));
return new Storage.Recipient(pair.getKey().id(), return new Storage.Recipient(pair.getKey().id(),
@ -651,7 +651,7 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile
String about, String about,
String aboutEmoji, String aboutEmoji,
String avatarUrlPath, String avatarUrlPath,
String paymentAddress, String mobileCoinAddress,
String unidentifiedAccessMode, String unidentifiedAccessMode,
Set<String> capabilities Set<String> capabilities
) {} ) {}

View file

@ -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;
}
}

View file

@ -1,14 +1,21 @@
package org.asamk.signal.manager.util; package org.asamk.signal.manager.util;
import com.google.protobuf.InvalidProtocolBufferException;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.storage.recipients.Profile; 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.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException; import org.whispersystems.signalservice.api.crypto.InvalidCiphertextException;
import org.whispersystems.signalservice.api.crypto.ProfileCipher; import org.whispersystems.signalservice.api.crypto.ProfileCipher;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; 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.Base64;
import java.util.HashSet; import java.util.HashSet;
@ -20,6 +27,12 @@ public class ProfileUtils {
final ProfileKey profileKey, final SignalServiceProfile encryptedProfile final ProfileKey profileKey, final SignalServiceProfile encryptedProfile
) { ) {
var profileCipher = new ProfileCipher(profileKey); var profileCipher = new ProfileCipher(profileKey);
IdentityKey identityKey = null;
try {
identityKey = new IdentityKey(Base64.getDecoder().decode(encryptedProfile.getIdentityKey()), 0);
} catch (InvalidKeyException ignored) {
}
try { try {
var name = decrypt(encryptedProfile.getName(), profileCipher); var name = decrypt(encryptedProfile.getName(), profileCipher);
var about = trimZeros(decrypt(encryptedProfile.getAbout(), profileCipher)); var about = trimZeros(decrypt(encryptedProfile.getAbout(), profileCipher));
@ -32,7 +45,11 @@ public class ProfileUtils {
about, about,
aboutEmoji, aboutEmoji,
encryptedProfile.getAvatar(), encryptedProfile.getAvatar(),
encryptedProfile.getPaymentAddress(), identityKey == null || encryptedProfile.getPaymentAddress() == null
? null
: decryptAndVerifyMobileCoinAddress(encryptedProfile.getPaymentAddress(),
profileCipher,
identityKey.getPublicKey()),
getUnidentifiedAccessMode(encryptedProfile, profileCipher), getUnidentifiedAccessMode(encryptedProfile, profileCipher),
getCapabilities(encryptedProfile)); getCapabilities(encryptedProfile));
} catch (InvalidCiphertextException e) { } 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) { private static Pair<String, String> splitName(String name) {
if (name == null) { if (name == null) {
return new Pair<>(null, null); return new Pair<>(null, null);

View file

@ -81,10 +81,10 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
r.getProfile().getFamilyName(), r.getProfile().getFamilyName(),
r.getProfile().getAbout(), r.getProfile().getAbout(),
r.getProfile().getAboutEmoji(), r.getProfile().getAboutEmoji(),
r.getProfile().getPaymentAddress() == null r.getProfile().getMobileCoinAddress() == null
? null ? null
: Base64.getEncoder() : Base64.getEncoder()
.encodeToString(r.getProfile().getPaymentAddress()))); .encodeToString(r.getProfile().getMobileCoinAddress())));
}).toList(); }).toList();
writer.write(jsonContacts); writer.write(jsonContacts);
@ -92,12 +92,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
} }
private record JsonContact( private record JsonContact(
String number, String number, String uuid, String name, boolean isBlocked, int messageExpirationTime, JsonProfile profile
String uuid,
String name,
boolean isBlocked,
int messageExpirationTime,
JsonProfile profile
) { ) {
private record JsonProfile( private record JsonProfile(
@ -106,7 +101,7 @@ public class ListContactsCommand implements JsonRpcLocalCommand {
String familyName, String familyName,
String about, String about,
String aboutEmoji, String aboutEmoji,
String paymentAddress String mobileCoinAddress
) {} ) {}
} }
} }