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