radix_transactions/model/v1/
message.rs

1use super::*;
2use crate::internal_prelude::*;
3
4/// Transaction messages as per REP-70
5#[derive(Debug, Clone, Eq, PartialEq, ManifestSbor, ScryptoDescribe)]
6pub enum MessageV1 {
7    None,
8    Plaintext(PlaintextMessageV1),
9    Encrypted(EncryptedMessageV1),
10}
11
12impl Default for MessageV1 {
13    fn default() -> Self {
14        Self::None
15    }
16}
17
18//============================================================================
19// PLAINTEXT MESSAGE
20//============================================================================
21
22#[derive(Debug, Clone, PartialEq, Eq, ManifestSbor, ScryptoDescribe)]
23pub struct PlaintextMessageV1 {
24    pub mime_type: String,
25    pub message: MessageContentsV1,
26}
27
28impl PlaintextMessageV1 {
29    pub fn text(message: impl Into<String>) -> Self {
30        Self {
31            mime_type: "text/plain".to_string(),
32            message: MessageContentsV1::String(message.into()),
33        }
34    }
35}
36
37/// We explicitly mark content as either String or Bytes - this distinguishes (along with the mime type)
38/// whether the message is intended to be displayable as text, or not.
39///
40/// This data model ensures that messages intended to be displayable as text are valid unicode strings.
41#[derive(Debug, Clone, PartialEq, Eq, ManifestSbor, ScryptoDescribe)]
42pub enum MessageContentsV1 {
43    String(String),
44    Bytes(Vec<u8>),
45}
46
47impl MessageContentsV1 {
48    pub fn len(&self) -> usize {
49        match self {
50            MessageContentsV1::String(message) => message.len(),
51            MessageContentsV1::Bytes(message) => message.len(),
52        }
53    }
54}
55
56//============================================================================
57// ENCRYPTED MESSAGE
58//============================================================================
59
60/// A `PlaintextMessageV1` encrypted with "MultiPartyECIES" for a number of decryptors (public keys).
61///
62/// First, a `PlaintextMessageV1` should be created, and encoded as `manifest_sbor_encode(plaintext_message)`
63/// to get the plaintext message payload bytes.
64///
65/// The plaintext message payload bytes are encrypted via (128-bit) AES-GCM with an ephemeral symmetric key.
66///
67/// The (128-bit) AES-GCM symmetric key is encrypted separately for each decryptor public key via (256-bit) AES-KeyWrap.
68/// AES-KeyWrap uses a key derived via a KDF (Key Derivation Function) using a shared secret.
69/// For each decryptor public key, we create a shared curve point `G` via static Diffie-Helman between the
70/// decryptor public key, and a per-transaction ephemeral public key for that curve type.
71/// We then use that shared secret with a key derivation function to create the (256-bit) KEK (Key Encrypting Key):
72/// `KEK = HKDF(hash: Blake2b, secret: x co-ord of G, salt: [], length: 256 bits)`.
73///
74/// Note:
75/// - For ECDH, the secret we use is the `x` coordinate of the shared public point, unhashed. This ECDH output is
76///   known as ASN1 X9.63 variant of ECDH. Be careful - libsecp256k1 uses another non-standard variant.
77/// - We persist 128-bit symmetric keys because we wish to save on payload size, and:
78///   * 128-bit AES is considered secure enough for most use cases (EG bitcoin hash rate is only 2^93 / year)
79///   * It's being used with a transient key - so a hypothetical successful attack would only decrypt one message
80#[derive(Debug, Clone, PartialEq, Eq, ManifestSbor, ScryptoDescribe)]
81pub struct EncryptedMessageV1 {
82    pub encrypted: AesGcmPayload,
83    // Note we use a collection here rather than a struct to be forward-compatible to adding more curve types.
84    // The engine should validate each DecryptorsByCurve matches the CurveType.
85    pub decryptors_by_curve: IndexMap<CurveType, DecryptorsByCurve>,
86}
87
88#[derive(
89    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ManifestSbor, ScryptoDescribe,
90)]
91pub enum CurveType {
92    Ed25519,
93    Secp256k1,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, ManifestSbor, ScryptoDescribe)]
97pub enum DecryptorsByCurve {
98    Ed25519 {
99        dh_ephemeral_public_key: Ed25519PublicKey,
100        decryptors: IndexMap<PublicKeyFingerprint, AesWrapped128BitKey>,
101    },
102    Secp256k1 {
103        dh_ephemeral_public_key: Secp256k1PublicKey,
104        decryptors: IndexMap<PublicKeyFingerprint, AesWrapped128BitKey>,
105    },
106}
107
108impl DecryptorsByCurve {
109    pub fn curve_type(&self) -> CurveType {
110        match self {
111            DecryptorsByCurve::Ed25519 { .. } => CurveType::Ed25519,
112            DecryptorsByCurve::Secp256k1 { .. } => CurveType::Secp256k1,
113        }
114    }
115
116    pub fn number_of_decryptors(&self) -> usize {
117        match self {
118            DecryptorsByCurve::Ed25519 { decryptors, .. } => decryptors.len(),
119            DecryptorsByCurve::Secp256k1 { decryptors, .. } => decryptors.len(),
120        }
121    }
122}
123
124/// The last 8 bytes of the Blake2b-256 hash of the public key bytes,
125/// in their standard Radix byte-serialization.
126#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, ManifestSbor, ScryptoDescribe)]
127#[sbor(transparent)]
128pub struct PublicKeyFingerprint(pub [u8; Self::LENGTH]);
129
130impl PublicKeyFingerprint {
131    pub const LENGTH: usize = 8;
132}
133
134impl From<PublicKey> for PublicKeyFingerprint {
135    fn from(value: PublicKey) -> Self {
136        value.get_hash().into()
137    }
138}
139
140impl From<PublicKeyHash> for PublicKeyFingerprint {
141    fn from(value: PublicKeyHash) -> Self {
142        let hash_bytes = value.get_hash_bytes();
143        let fingerprint_bytes = &hash_bytes[(hash_bytes.len() - Self::LENGTH)..hash_bytes.len()];
144        PublicKeyFingerprint(copy_u8_array(fingerprint_bytes))
145    }
146}
147
148/// The (128-bit) AES-GCM encrypted bytes of the payload.
149///
150/// This must be serialized as the concatenation `Nonce/IV || Cipher || Tag/MAC` where:
151/// * Nonce/IV: 12 bytes
152/// * Cipher(text): Variable length
153/// * Tag/MAC: 16 bytes
154#[derive(Debug, Clone, Eq, PartialEq, ManifestSbor, ScryptoDescribe)]
155#[sbor(transparent)]
156pub struct AesGcmPayload(pub Vec<u8>);
157
158/// The wrapped key bytes from applying 256-bit AES-KeyWrap from RFC-3394
159/// to the 128-bit message ephemeral public key, with the secret KEK provided by
160/// static Diffie-Helman between the decryptor public key, and the `dh_ephemeral_public_key`
161/// for that curve type.
162///
163/// This must be serialized as per https://www.ietf.org/rfc/rfc3394.txt as `IV || Cipher` where:
164/// * IV: First 8 bytes
165/// * Cipher: The wrapped 128 bit key, encoded as two 64 bit blocks
166#[derive(Debug, Clone, Eq, PartialEq, ManifestSbor, ScryptoDescribe)]
167#[sbor(transparent)]
168pub struct AesWrapped128BitKey(pub [u8; Self::LENGTH]);
169
170impl AesWrapped128BitKey {
171    /// 8 bytes IV, and then the encoded key
172    pub const LENGTH: usize = 24;
173}
174
175//============================================================================
176// PREPARATION
177//============================================================================
178
179#[allow(deprecated)]
180pub type PreparedMessageV1 = SummarizedRawFullValue<MessageV1>;
181
182// TODO: Add tests with a canonical implementation of message encryption/decryption,
183// and corresponding test vectors for other implementers.