Skip to main content

miden_core_lib/handlers/
aead_decrypt.rs

1//! AEAD decryption event handler for the Miden VM.
2//!
3//! This module provides an event handler for decrypting AEAD ciphertext using non-deterministic
4//! advice. When the VM emits an AEAD_DECRYPT_EVENT, this handler reads the ciphertext from memory,
5//! performs decryption using the AEAD-Poseidon2 scheme, and pushes the plaintext onto the advice
6//! stack for the MASM decrypt procedure to load.
7
8use alloc::{vec, vec::Vec};
9
10use miden_core::events::EventName;
11use miden_crypto::aead::{
12    DataType, EncryptionError,
13    aead_poseidon2::{AuthTag, EncryptedData, Nonce, SecretKey},
14};
15use miden_processor::{ProcessorState, advice::AdviceMutation, event::EventError};
16
17use crate::handlers::read_memory_region;
18
19/// Qualified event name for the AEAD decrypt event.
20pub const AEAD_DECRYPT_EVENT_NAME: EventName = EventName::new("miden::core::crypto::aead::decrypt");
21
22/// Event handler for AEAD decryption.
23///
24/// This handler is called when the VM emits an AEAD_DECRYPT_EVENT. It reads the full
25/// ciphertext (including padding block) and tag from memory, performs decryption and
26/// tag verification using AEAD-Poseidon2, then pushes the plaintext onto the advice stack.
27///
28/// Process:
29/// 1. Reads full ciphertext from memory at src_ptr ((num_blocks + 1) * 8 elements)
30/// 2. Reads authentication tag from memory at src_ptr + (num_blocks + 1) * 8
31/// 3. Constructs EncryptedData and decrypts using AEAD-Poseidon2
32/// 4. Extracts only the data blocks (first num_blocks * 8 elements) from plaintext
33/// 5. Pushes the data blocks (WITHOUT padding) onto the advice stack in reverse order
34///
35/// Memory layout at src_ptr:
36/// - [ciphertext_blocks(num_blocks * 8), encrypted_padding(8), tag(4)]
37/// - This handler reads ALL elements: data blocks + padding + tag
38///
39/// The MASM decrypt procedure will then:
40/// 1. Load the plaintext data blocks from advice stack and write to dst_ptr using adv_pipe
41/// 2. Call encrypt which reads the data blocks and adds padding automatically
42/// 3. Re-encrypt data + padding to compute authentication tag
43/// 4. Compare computed tag with expected tag and halt if they don't match
44///
45/// Non-determinism soundness: Using advice for decryption is cryptographically sound
46/// because:
47/// 1. The MASM procedure re-verifies the tag when decrypting
48/// 2. The deterministic encryption creates a bijection between plaintext and ciphertext
49/// 3. A malicious prover cannot provide incorrect plaintext without causing tag mismatch
50pub fn handle_aead_decrypt(process: &ProcessorState) -> Result<Vec<AdviceMutation>, EventError> {
51    // Stack: [event_id, key(4), nonce(4), src_ptr, dst_ptr, num_blocks, ...]
52    // where:
53    //   src_ptr = ciphertext + encrypted_padding + tag location (input)
54    //   dst_ptr = plaintext destination (output)
55    //   num_blocks = number of plaintext data blocks (NO padding)
56
57    // Read parameters from stack
58    // Note: Stack position 0 contains the Event ID when the handler is called,
59    // so the actual parameters start at position 1. Words on the stack are
60    // interpreted in little-endian (memory) order, i.e. element at stack index N
61    // becomes the first limb of the word.
62    let key_word = process.get_stack_word(1);
63    let nonce_word = process.get_stack_word(5);
64
65    let src_ptr = process.get_stack_item(9).as_canonical_u64();
66    let num_blocks = process.get_stack_item(11).as_canonical_u64();
67
68    let (num_ciphertext_elements, tag_ptr, data_blocks_count) = compute_sizes(num_blocks, src_ptr)?;
69
70    // Read ciphertext from memory: (num_blocks + 1) * 8 elements (data + padding)
71    let ciphertext = read_memory_region(process, src_ptr, num_ciphertext_elements).ok_or(
72        AeadDecryptError::MemoryReadFailed {
73            addr: src_ptr,
74            len: num_ciphertext_elements,
75        },
76    )?;
77
78    // Read authentication tag: 4 elements (1 word) immediately after ciphertext
79    let tag_addr: u32 = tag_ptr
80        .try_into()
81        .ok()
82        .ok_or(AeadDecryptError::MemoryReadFailed { addr: tag_ptr, len: 4 })?;
83
84    let ctx = process.ctx();
85    let tag_word = process
86        .get_mem_word(ctx, tag_addr)
87        .map_err(|_| AeadDecryptError::MemoryReadFailed { addr: tag_ptr, len: 4 })?
88        .ok_or(AeadDecryptError::MemoryReadFailed { addr: tag_ptr, len: 4 })?;
89
90    let tag_elements: [miden_core::Felt; 4] = tag_word.into();
91
92    // Convert to reference implementation types
93    let secret_key = SecretKey::from_elements(key_word.into());
94    let nonce = Nonce::from(nonce_word);
95    let auth_tag = AuthTag::new(tag_elements);
96
97    // Construct EncryptedData
98    let encrypted_data =
99        EncryptedData::from_parts(DataType::Elements, ciphertext, auth_tag, nonce.clone());
100
101    // Decrypt using the standard reference implementation
102    // This performs tag verification internally
103    let plaintext_with_padding = secret_key.decrypt_elements(&encrypted_data)?;
104
105    // Extract only the data blocks (without padding) to push onto advice stack
106    // The MASM encrypt procedure will add padding automatically during re-encryption
107    let mut plaintext_data = plaintext_with_padding;
108    plaintext_data.truncate(data_blocks_count);
109
110    // Push plaintext data (WITHOUT padding) onto advice stack.
111    // Values are provided in structural order; `extend_stack` ensures the first element
112    // ends up at the top of the advice stack, matching `adv_pipe` expectations.
113    let advice_stack_mutation = AdviceMutation::extend_stack(plaintext_data);
114
115    Ok(vec![advice_stack_mutation])
116}
117
118fn compute_sizes(num_blocks: u64, src_ptr: u64) -> Result<(u64, u64, usize), AeadDecryptError> {
119    let num_ciphertext_elements = num_blocks
120        .checked_add(1)
121        .and_then(|blocks| blocks.checked_mul(8))
122        .ok_or(AeadDecryptError::SizeOverflow)?;
123    let tag_ptr = src_ptr
124        .checked_add(num_ciphertext_elements)
125        .ok_or(AeadDecryptError::SizeOverflow)?;
126    let data_blocks_count = num_blocks
127        .checked_mul(8)
128        .and_then(|count| count.try_into().ok())
129        .ok_or(AeadDecryptError::SizeOverflow)?;
130
131    Ok((num_ciphertext_elements, tag_ptr, data_blocks_count))
132}
133
134// ERROR HANDLING
135// ================================================================================================
136
137/// Error types that can occur during AEAD decryption.
138#[derive(Debug, thiserror::Error)]
139enum AeadDecryptError {
140    /// Memory read failed or address overflow.
141    #[error("failed to read memory region at addr={addr}, len={len}")]
142    MemoryReadFailed { addr: u64, len: u64 },
143
144    /// Size or address computation overflowed.
145    #[error("size overflow in AEAD decrypt handler")]
146    SizeOverflow,
147
148    /// Decryption failed (wraps EncryptionError from miden-crypto).
149    #[error(transparent)]
150    DecryptionFailed(#[from] EncryptionError),
151}
152
153// TESTS
154// ================================================================================================
155
156#[cfg(test)]
157mod tests {
158    use crate::handlers::aead_decrypt::{AEAD_DECRYPT_EVENT_NAME, AeadDecryptError, compute_sizes};
159
160    #[test]
161    fn test_event_name() {
162        assert_eq!(AEAD_DECRYPT_EVENT_NAME.as_str(), "miden::core::crypto::aead::decrypt");
163    }
164
165    #[test]
166    fn test_compute_sizes_happy_path() {
167        let (num_ciphertext_elements, tag_ptr, data_blocks_count) =
168            compute_sizes(1, 0).expect("sizes should fit");
169        assert_eq!(num_ciphertext_elements, 16);
170        assert_eq!(tag_ptr, 16);
171        assert_eq!(data_blocks_count, 8);
172    }
173
174    #[test]
175    fn test_compute_sizes_overflow_num_blocks() {
176        let err = compute_sizes(u64::MAX, 0).expect_err("should overflow");
177        assert!(matches!(err, AeadDecryptError::SizeOverflow));
178    }
179
180    #[test]
181    fn test_compute_sizes_overflow_tag_ptr() {
182        let err = compute_sizes(0, u64::MAX).expect_err("should overflow tag ptr");
183        assert!(matches!(err, AeadDecryptError::SizeOverflow));
184    }
185
186    #[cfg(target_pointer_width = "32")]
187    #[test]
188    fn test_compute_sizes_overflow_data_blocks_count() {
189        let num_blocks = (usize::MAX as u64 / 8) + 1;
190        let err = compute_sizes(num_blocks, 0).expect_err("should overflow usize");
191        assert!(matches!(err, AeadDecryptError::SizeOverflow));
192    }
193}