bc_components/symmetric/encrypted_message.rs
1use bc_ur::prelude::*;
2
3use crate::{AuthenticationTag, Digest, DigestProvider, Nonce, tags};
4
5/// A secure encrypted message using IETF ChaCha20-Poly1305 authenticated
6/// encryption.
7///
8/// `EncryptedMessage` represents data that has been encrypted using a symmetric
9/// key with the ChaCha20-Poly1305 AEAD (Authenticated Encryption with
10/// Associated Data) construction as specified in [RFC-8439](https://datatracker.ietf.org/doc/html/rfc8439).
11///
12/// An `EncryptedMessage` contains:
13/// - `ciphertext`: The encrypted data (same length as the original plaintext)
14/// - `aad`: Additional Authenticated Data that is not encrypted but is
15/// authenticated (optional)
16/// - `nonce`: A 12-byte number used once for this specific encryption operation
17/// - `auth`: A 16-byte authentication tag that verifies the integrity of the
18/// message
19///
20/// The `aad` field is often used to include the `Digest` of the plaintext,
21/// which allows verification of the plaintext after decryption and preserves
22/// the unique identity of the data when used with structures like Gordian
23/// Envelope.
24///
25/// To facilitate decoding, it is recommended that the plaintext of an
26/// `EncryptedMessage` be tagged CBOR.
27///
28/// CDDL:
29/// ```cddl
30/// EncryptedMessage =
31/// #6.40002([ ciphertext: bstr, nonce: bstr, auth: bstr, ? aad: bstr ]) ; TAG_ENCRYPTED
32/// ```
33#[derive(Clone, Eq, PartialEq)]
34pub struct EncryptedMessage {
35 ciphertext: Vec<u8>,
36 aad: Vec<u8>, // Additional authenticated data (AAD) per RFC8439
37 nonce: Nonce,
38 auth: AuthenticationTag,
39}
40
41impl EncryptedMessage {
42 /// Restores an EncryptedMessage from its CBOR representation.
43 ///
44 /// This is a low-level function that is not normally needed.
45 pub fn new(
46 ciphertext: impl AsRef<[u8]>,
47 aad: impl AsRef<[u8]>,
48 nonce: Nonce,
49 auth: AuthenticationTag,
50 ) -> Self {
51 Self {
52 ciphertext: ciphertext.as_ref().to_vec(),
53 aad: aad.as_ref().to_vec(),
54 nonce,
55 auth,
56 }
57 }
58
59 /// Returns a reference to the ciphertext data.
60 pub fn ciphertext(&self) -> &[u8] { &self.ciphertext }
61
62 /// Returns a reference to the additional authenticated data (AAD).
63 pub fn aad(&self) -> &[u8] { &self.aad }
64
65 /// Returns a reference to the nonce value used for encryption.
66 pub fn nonce(&self) -> &Nonce { &self.nonce }
67
68 /// Returns a reference to the authentication tag value used for encryption.
69 pub fn authentication_tag(&self) -> &AuthenticationTag { &self.auth }
70
71 /// Returns a CBOR representation in the AAD field, if it exists.
72 pub fn aad_cbor(&self) -> Option<CBOR> {
73 if self.aad.is_empty() {
74 None
75 } else {
76 CBOR::try_from_data(self.aad()).ok()
77 }
78 }
79
80 /// Returns a `Digest` instance if the AAD data can be parsed as
81 /// CBOR.
82 pub fn aad_digest(&self) -> Option<Digest> {
83 self.aad_cbor().and_then(|cbor| Digest::try_from(cbor).ok())
84 }
85
86 /// Returns `true` if the AAD data can be parsed as CBOR.
87 pub fn has_digest(&self) -> bool { self.aad_digest().is_some() }
88}
89
90/// Implements Debug formatting to display the message contents in a structured
91/// format.
92impl std::fmt::Debug for EncryptedMessage {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 f.debug_struct("EncryptedMessage")
95 .field("ciphertext", &hex::encode(&self.ciphertext))
96 .field("aad", &hex::encode(&self.aad))
97 .field("nonce", &self.nonce)
98 .field("auth", &self.auth)
99 .finish()
100 }
101}
102
103/// Implements `AsRef<EncryptedMessage>` to allow self-reference.
104impl AsRef<EncryptedMessage> for EncryptedMessage {
105 fn as_ref(&self) -> &EncryptedMessage { self }
106}
107
108/// Implements DigestProvider to provide the digest stored in the AAD field.
109impl DigestProvider for EncryptedMessage {
110 fn digest(&self) -> Digest { self.aad_digest().unwrap() }
111}
112
113/// Implements CBORTagged to provide the CBOR tag for the EncryptedMessage.
114impl CBORTagged for EncryptedMessage {
115 fn cbor_tags() -> Vec<Tag> { tags_for_values(&[tags::TAG_ENCRYPTED]) }
116}
117
118/// Implements conversion from EncryptedMessage to CBOR for serialization.
119impl From<EncryptedMessage> for CBOR {
120 fn from(value: EncryptedMessage) -> Self { value.tagged_cbor() }
121}
122
123/// Implements `TryFrom<CBOR>` for EncryptedMessage to support conversion from
124/// CBOR data.
125impl TryFrom<CBOR> for EncryptedMessage {
126 type Error = dcbor::Error;
127
128 fn try_from(cbor: CBOR) -> dcbor::Result<Self> {
129 Self::from_tagged_cbor(cbor)
130 }
131}
132
133/// Implements CBORTaggedEncodable to provide CBOR encoding functionality.
134impl CBORTaggedEncodable for EncryptedMessage {
135 fn untagged_cbor(&self) -> CBOR {
136 let mut a = vec![
137 CBOR::to_byte_string(&self.ciphertext),
138 CBOR::to_byte_string(self.nonce.data()),
139 CBOR::to_byte_string(self.auth.data()),
140 ];
141
142 if !self.aad.is_empty() {
143 a.push(CBOR::to_byte_string(&self.aad));
144 }
145
146 a.into()
147 }
148}
149
150/// Implements CBORTaggedDecodable to provide CBOR decoding functionality.
151impl CBORTaggedDecodable for EncryptedMessage {
152 fn from_untagged_cbor(cbor: CBOR) -> dcbor::Result<Self> {
153 match cbor.as_case() {
154 CBORCase::Array(elements) => {
155 if elements.len() < 3 {
156 return Err(
157 "EncryptedMessage must have at least 3 elements".into(),
158 );
159 }
160 let ciphertext =
161 CBOR::try_into_byte_string(elements[0].clone())?;
162 let nonce_data =
163 CBOR::try_into_byte_string(elements[1].clone())?;
164 let nonce = Nonce::from_data_ref(nonce_data)?;
165 let auth_data =
166 CBOR::try_into_byte_string(elements[2].clone())?;
167 let auth = AuthenticationTag::from_data_ref(auth_data)?;
168 let aad = if elements.len() > 3 {
169 CBOR::try_into_byte_string(elements[3].clone())?
170 } else {
171 Vec::new()
172 };
173 Ok(Self::new(ciphertext, aad, nonce, auth))
174 }
175 _ => Err("EncryptedMessage must be an array".into()),
176 }
177 }
178}