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.