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}