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.