How to decrypt an encrypted transaction message created by the Radix Wallet

How to decrypt an encrypted transaction message created by the Radix Wallet

0. Introduction

The encryption scheme used by the Radix Wallet is called DH_ADD_EPH_AESGCM256_SCRYPT_000

Where:

  • DH → (Elliptic Curve) Diffie-Hellman (A key exchange protocol that allows untrusted parties to construct a shared secret)
  • ADD → Elliptic Curve Point Addition (The operation we use to create the shared secret)
  • EPH → Ephemeral Public Key (A temporary public key that is only used for the purpose of encrypting/decrypting the message)
  • AESGCM256 → The symmetrical encryption algorithm used to encrypt/decrypt the message (Advanced Encryption Standard, Galios Counter Mode, 256 bits)
  • SCRYPT → The Key Derivation Function (KDF) used (Scrypt)
  • 000 → The version of the scheme

The encryption scheme allows both the sender and receiver to decrypt the message - but no one else.

Alice sends the following encrypted message to Bob:

01ff02663a6aaf4d5ec607330b9b74a840bf5c13b0a7357202fa85be56b1326065561657d6ee46d4d84e94ec615b425a472dd8c813bad125335a097d29b64b72319357406b2b04491b4ca1a5a05fe8772b0c05f4633b399914348c5b03af58445d42c2f740f8407e572775a571805e582c6b96ffd4ccca764f2002510abddaab735ee4fb0b18c26d

Alice’s Wallet Address: rdx1qsp8n0nx0muaewav2ksx99wwsu9swq5mlndjmn3gm9vl9q2mzmup0xqm2ylge

Bob’s Wallet Address: rdx1qspvvprlj3q76ltdxpz5qm54cp7dshrh3e9cemeu5746czdet3cfaegp8alwf

For Bob to decrypt the message, he needs:

  • Alice’s Public Key
  • Bob’s Private Key

1. Convert Alice’s Wallet address into a her Public Key:

import bech32

alice_wallet_address = "rdx1qsp8n0nx0muaewav2ksx99wwsu9swq5mlndjmn3gm9vl9q2mzmup0xqm2ylge"

_hrp, alice_readdr_5bit = bech32.bech32_decode(alice_wallet_address)
alice_readdr_bytes = bech32.convertbits(alice_readdr_5bit, 5, 8, pad=False)

## Remove the REAddr 04 prefix byte
alice_public_key_bytes = bytes(alice_readdr_bytes)[1:34]

alice_public_key_hex = bytes(alice_public_key_bytes).hex()
print("Alice Public Key:", alice_public_key_hex)

# Alice Public Key: 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798

2. Use Elliptic Curve Diffie-Helmann to combine Alice’s Public Key with Bob’s Private Key:

import hashlib
from ecdsa.curves import SECP256k1
from ecdsa.keys import SigningKey, VerifyingKey

alice_public_key_hex = "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"
alice_public_key = VerifyingKey.from_string(bytearray.fromhex(alice_public_key_hex), curve=SECP256k1, hashfunc=hashlib.sha256 )

bob_private_key_hex = "0000000000000000000000000000000000000000000000000000000000000002"
bob_private_key = SigningKey.from_string(bytearray.fromhex(bob_private_key_hex), curve=SECP256k1, hashfunc=hashlib.sha256)

# Diffie-Hellman - we need to use lower level functions here so that we get back dh as a curve point
dh = alice_public_key.pubkey.point * bob_private_key.privkey.secret_multiplier

Note: Although it looks like a simple multiplication operation, the * operator has been overridden in the ECDSA library to perform Elliptic Curve Diffie-Hellman calculation

3. Slice the encrypted message into its component parts:

encrypted_message = "01ff02663a6aaf4d5ec607330b9b74a840bf5c13b0a7357202fa85be56b1326065561657d6ee46d4d84e94ec615b425a472dd8c813bad125335a097d29b64b72319357406b2b04491b4ca1a5a05fe8772b0c05f4633b399914348c5b03af58445d42c2f740f8407e572775a571805e582c6b96ffd4ccca764f2002510abddaab735ee4fb0b18c26d"

# Message Type: 01 (1 byte)
message_type = encrypted_message[0:2]

# Encryption Type: ff (1 byte)
encryption_type = encrypted_message[2:4]

# Ephemeral Public Key: 02663a6aaf4d5ec607330b9b74a840bf5c13b0a7357202fa85be56b13260655616 (33 bytes)
ephemeral_public_key = encrypted_message[4:70]

# Nonce: 57d6ee46d4d84e94ec615b42 (12 bytes)
nonce = encrypted_message[70:94]

# Auth Tag: 5a472dd8c813bad125335a097d29b64b (16 bytes)
auth_tag = encrypted_message[94:126]

# Ciphertext: 72319357406b2b04491b4ca1a5a05fe8772b0c05f4633b399914348c5b03af58445d42c2f740f8407e572775a571805e582c6b96ffd4ccca764f2002510abddaab735ee4fb0b18c26d
ciphertext = encrypted_message[126:]

4. Convert the Ephemeral Public Key into a Curve Point

# Convert Ephemeral Public key into a Curve Point
ephemeral_curve = VerifyingKey.from_string(bytearray.fromhex(ephemeral_public_key), curve=ecdsa.SECP256k1, hashfunc=hashlib.sha256)
ephemeral_point = ephemeral_curve.pubkey.point

5. Use Elliptic Curve Point Addition to combine the DH Point with the Ephemeral Point

# Elliptic Curve Point Addition
shared_secret_point = dh + ephemeral_point

Note: Although it looks like a simple addition operation, the + operator has been overridden in the ECDSA library to perform Elliptic Curve Point Addition

6. Use the x co-ordinate of the Shared Secret Point as the Shared Secret

shared_secret_integer = shared_secret_point.x()
shared_secret_hex = hex(shared_secret_integer)[2:]

print("Shared Secret:", shared_secret_hex)

# Shared Secret: 64567aaa53dadd0d9dade89aca0beba4ea00ff987d986f5af421ff2bd636ca9d

7. Use the Scrypt Key Derivation Function (KDF) to convert the shared secret into the message decryption key:

from Crypto.Protocol.KDF import scrypt

# Create the Scrypt salt from the SHA256 of the nonce
salt = hashlib.sha256(bytearray.fromhex(nonce)).digest()

# Get the Decryption Key
key = scrypt(bytearray.fromhex(shared_secret_hex), salt, 32, 8192, 8 ,1)

print("Decryption Key:", key.hex())

# Decryption Key: 7a294fd759ffdba936f46f5c773c7c8bd3e44064572b926e4e3e0b7291897a30

8. Decrypt the cipher text with the decryption key using AES-GCM:

from Crypto.Cipher import AES

decrypt = AES.new(key, AES.MODE_GCM, nonce=bytearray.fromhex(nonce))

# Add the Ephemeral Public Key as Associated Data
decrypt.update(bytearray.fromhex(ephemeral_public_key))

msg = decrypt.decrypt_and_verify(bytearray.fromhex(ciphertext), bytearray.fromhex(auth_tag))

print("Plaintext Message:", msg)

# Plaintext Message: b'Hey Bob, this is Alice, you and I can read this message, but no one else.'

8. The Entire Script:

import bech32
import ecdsa
import hashlib
from ecdsa.curves import SECP256k1
from ecdsa.keys import SigningKey, VerifyingKey
from Crypto.Protocol.KDF import scrypt
from Crypto.Cipher import AES


encrypted_message = "01ff02663a6aaf4d5ec607330b9b74a840bf5c13b0a7357202fa85be56b1326065561657d6ee46d4d84e94ec615b425a472dd8c813bad125335a097d29b64b72319357406b2b04491b4ca1a5a05fe8772b0c05f4633b399914348c5b03af58445d42c2f740f8407e572775a571805e582c6b96ffd4ccca764f2002510abddaab735ee4fb0b18c26d"
alice_wallet_address = "rdx1qsp8n0nx0muaewav2ksx99wwsu9swq5mlndjmn3gm9vl9q2mzmup0xqm2ylge"
bob_private_key_hex = "0000000000000000000000000000000000000000000000000000000000000002"


_hrp, alice_readdr_5bit = bech32.bech32_decode(alice_wallet_address)
alice_readdr_bytes = bech32.convertbits(alice_readdr_5bit, 5, 8, pad=False)

## Remove the REAddr 04 prefix byte
alice_public_key_bytes = bytes(alice_readdr_bytes)[1:34]

alice_public_key_hex = bytes(alice_public_key_bytes).hex()
print("Alice Public Key:", alice_public_key_hex)

alice_public_key = VerifyingKey.from_string(bytearray.fromhex(alice_public_key_hex), curve=SECP256k1, hashfunc=hashlib.sha256 )

bob_private_key_hex = "0000000000000000000000000000000000000000000000000000000000000002"
bob_private_key = SigningKey.from_string(bytearray.fromhex(bob_private_key_hex), curve=SECP256k1, hashfunc=hashlib.sha256)

# Diffie-Hellman - we need to use lower level functions here so that we get back dh as a curve point
dh = alice_public_key.pubkey.point * bob_private_key.privkey.secret_multiplier

# Message Type: 01 (1 byte)
message_type = encrypted_message[1:2]

# Encryption Type: ff (1 byte)
encryption_type = encrypted_message[2:4]

# Ephemeral Public Key: 02663a6aaf4d5ec607330b9b74a840bf5c13b0a7357202fa85be56b13260655616 (33 bytes)
ephemeral_public_key = encrypted_message[4:70]

# Nonce: 57d6ee46d4d84e94ec615b42 (12 bytes)
nonce = encrypted_message[70:94]

# Auth Tag: 5a472dd8c813bad125335a097d29b64b (16 bytes)
auth_tag = encrypted_message[94:126]

# Ciphertext: 72319357406b2b04491b4ca1a5a05fe8772b0c05f4633b399914348c5b03af58445d42c2f740f8407e572775a571805e582c6b96ffd4ccca764f2002510abddaab735ee4fb0b18c26d
ciphertext = encrypted_message[126:]

# Convert Ephemeral Public key into a Curve Point
ephemeral_curve = VerifyingKey.from_string(bytearray.fromhex(ephemeral_public_key), curve=ecdsa.SECP256k1, hashfunc=hashlib.sha256)
ephemeral_point = ephemeral_curve.pubkey.point

# Elliptic Curve Point Addition
shared_secret_point = dh + ephemeral_point

shared_secret_integer = shared_secret_point.x()
shared_secret_hex = hex(shared_secret_integer)[2:]

print("Shared Secret:", shared_secret_hex)

# Create the Scrypt salt from the SHA256 of the nonce
salt = hashlib.sha256(bytearray.fromhex(nonce)).digest()

# Get the Decryption Key
key = scrypt(bytearray.fromhex(shared_secret_hex), salt, 32, 8192, 8 ,1)

print("Decryption Key:", key.hex())

decrypt = AES.new(key, AES.MODE_GCM, nonce=bytearray.fromhex(nonce))

# Add the Ephemeral Public Key as Associated Data
decrypt.update(bytearray.fromhex(ephemeral_public_key))

msg = decrypt.decrypt_and_verify(bytearray.fromhex(ciphertext), bytearray.fromhex(auth_tag))

print("Plaintext Message:", msg)

Run this code on Replit

Also see: How to encrypt a transaction message that can be read by the Radix Wallet

This work by RadixPool.com is licensed under a Creative Commons Attribution 4.0 International License.

1 Like

Similarly, Alice can decrypt her message that she sent to Bob with:

  • Alice’s Private Key
  • Bob’s Public Key

DH = Alice Private Key * Bob Public Key = Alice Public Key * Bob Private Key

I have a small suggestion that this should be changed to message_type = encrypted_message[0:2] to get the string value of “01”

Also could you be able to verify the auth_tag ? I test with your hard coded data, then if we do AES encrypt with empty associatedData, then the auth_tag should be “101416c4f912797fe53f87c3f96f4374”

If the auth_tag is “5a472dd8c813bad125335a097d29b64b” then it means Alice encrypted via AES with some value of associatedData (this is a parameter name of AEADParameters in the pointycastle library)

Doh! Thanks, I’ve edited the code to fix that.

I originally used decrypt.decrypt_and_verify(bytearray.fromhex(ciphertext), bytearray.fromhex(auth_tag)) but the MAC (Message Authentication Code) check always failed. I didn’t notice the message was decrypting “correctly” until I split the operation into separate decrypt and then verify steps. Of course the verify step using the auth_tag should be working to ensure that the decrypted message is verifiably correct.

Yes, I think you’re right. The problem is almost certainly related to associated data. Associated data is not encrypted as part of the ciphertext but is used when calculating the auth_tag.

Once the auth_tag problem is fixed then I’ll be able to write up a guide to encrypting messages. Hopefully that will be very soon™…

The decrypt_and_verify function works correctly now. I had to add the following line to include the associated data before calling the decrypt_and_verify function:

decrypt.update(bytearray.fromhex(ephemeral_public_key))

Thanks, I tested and including the associated data will make the verify successfully, and port your code to pointycastle library also work correctly.