diff --git a/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr b/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr index 4197226eb618..46b6a7a361d9 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr @@ -20,7 +20,7 @@ use crate::{ get_arr_of_size__message_bytes__from_PT, get_arr_of_size__message_bytes_padding__from_PT, }, }, - oracle::{aes128_decrypt::aes128_decrypt_oracle, random::random, shared_secret::get_shared_secret}, + oracle::{aes128_decrypt::aes128_decrypt, random::random, shared_secret::get_shared_secret}, utils::{ array, conversion::{ @@ -425,7 +425,7 @@ impl MessageEncryption for AES128 { BoundedVec::::from_array(header_ciphertext); // Decrypt header - let header_plaintext = aes128_decrypt_oracle(header_ciphertext_bvec, header_iv, header_sym_key); + let header_plaintext = aes128_decrypt(header_ciphertext_bvec, header_iv, header_sym_key); // Extract ciphertext length from header (2 bytes, big-endian) extract_ciphertext_length(header_plaintext) @@ -439,7 +439,7 @@ impl MessageEncryption for AES128 { BoundedVec::from_parts(ciphertext_with_padding, ciphertext_length); // Decrypt main ciphertext and return it - let plaintext_bytes = aes128_decrypt_oracle(ciphertext, body_iv, body_sym_key); + let plaintext_bytes = aes128_decrypt(ciphertext, body_iv, body_sym_key); // Convert bytes back to fields (32 bytes per field). Returns None if the actual bytes are // not valid. diff --git a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr index 7874765a31f7..0569dd438dc1 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr @@ -1,3 +1,12 @@ +use crate::utils::array::assert_bounded_vec_trimmed; + +#[oracle(aztec_utl_aes128Decrypt)] +unconstrained fn aes128_decrypt_oracle( + ciphertext: BoundedVec, + iv: [u8; 16], + sym_key: [u8; 16], +) -> BoundedVec {} + /// Decrypts a ciphertext, using AES128. /// /// Returns a BoundedVec containing the plaintext. @@ -8,12 +17,15 @@ /// Note that we accept ciphertext as a BoundedVec, not as an array. This is because this function is typically used /// when processing logs and at that point we don't have a comptime information about the length of the ciphertext as /// the log is not specific to any individual note. -#[oracle(aztec_utl_aes128Decrypt)] -pub unconstrained fn aes128_decrypt_oracle( +pub unconstrained fn aes128_decrypt( ciphertext: BoundedVec, iv: [u8; 16], sym_key: [u8; 16], -) -> BoundedVec {} +) -> BoundedVec { + let result = aes128_decrypt_oracle(ciphertext, iv, sym_key); + assert_bounded_vec_trimmed(result); + result +} mod test { use crate::{ @@ -21,7 +33,7 @@ mod test { utils::{array::subarray::subarray, point::point_from_x_coord}, }; use crate::test::helpers::test_environment::TestEnvironment; - use super::aes128_decrypt_oracle; + use super::aes128_decrypt; use poseidon::poseidon2::Poseidon2; use std::aes128::aes128_encrypt; @@ -49,7 +61,7 @@ mod test { // ciphertext length is fixed. But we do it anyway to not have to have duplicate oracles. let ciphertext_bvec = BoundedVec::::from_array(ciphertext); - let received_plaintext = aes128_decrypt_oracle(ciphertext_bvec, iv, sym_key); + let received_plaintext = aes128_decrypt(ciphertext_bvec, iv, sym_key); assert_eq(received_plaintext.len(), TEST_PLAINTEXT_LENGTH); assert_eq(received_plaintext.max_len(), TEST_CIPHERTEXT_LENGTH); @@ -123,7 +135,7 @@ mod test { // We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's designed to // work with logs of unknown length. let ciphertext_bvec = BoundedVec::::from_array(ciphertext); - let received_plaintext = aes128_decrypt_oracle(ciphertext_bvec, iv, bad_sym_key); + let received_plaintext = aes128_decrypt(ciphertext_bvec, iv, bad_sym_key); let extracted_mac_as_bytes: [u8; TEST_MAC_LENGTH] = subarray(received_plaintext.storage(), TEST_PLAINTEXT_LENGTH); diff --git a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr index 1975fbabb999..bb5020cd5612 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr @@ -1,4 +1,5 @@ use crate::note::{HintedNote, note_interface::NoteType}; +use crate::utils::array::assert_bounded_vec_trimmed; use crate::protocol::{address::AztecAddress, traits::Packable}; @@ -142,6 +143,7 @@ where MaxNotes, as Packable>::N, ); + assert_bounded_vec_trimmed(packed_hinted_notes); let mut notes = BoundedVec::<_, MaxNotes>::new(); for i in 0..packed_hinted_notes.len() { diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/assert_trimmed.nr b/noir-projects/aztec-nr/aztec/src/utils/array/assert_trimmed.nr new file mode 100644 index 000000000000..784a51c4f3ba --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/utils/array/assert_trimmed.nr @@ -0,0 +1,66 @@ +/// Asserts that elements past `len()` in a `BoundedVec` are zeroed. +/// +/// Oracle functions may return `BoundedVec` values with dirty trailing storage (non-zero elements past `len()`). +/// This is problematic because `BoundedVec`'s `Eq` implementation and other operations assume trailing elements +/// are zeroed. +/// +/// This function should be called on any `BoundedVec` obtained from an oracle to guard against malformed data. +/// +/// TODO(https://github.com/noir-lang/noir/issues/4218): Remove once Noir natively validates `BoundedVec` returned +/// from unconstrained functions. +pub(crate) unconstrained fn assert_bounded_vec_trimmed(vec: BoundedVec) +where + T: Eq, +{ + let storage = vec.storage(); + let len = vec.len(); + for i in 0..MaxLen { + if i >= len { + assert_eq(storage[i], std::mem::zeroed(), "BoundedVec has non-zero trailing elements"); + } + } +} + +mod test { + use super::assert_bounded_vec_trimmed; + + #[test] + unconstrained fn trimmed_empty_vec() { + let vec: BoundedVec = BoundedVec::new(); + assert_bounded_vec_trimmed(vec); + } + + #[test] + unconstrained fn trimmed_full_vec() { + let vec = BoundedVec::::from_array([1, 2, 3]); + assert_bounded_vec_trimmed(vec); + } + + #[test] + unconstrained fn trimmed_partial_vec() { + let vec = BoundedVec::::from_array([1, 2, 3]); + assert_bounded_vec_trimmed(vec); + } + + #[test(should_fail_with = "BoundedVec has non-zero trailing elements")] + unconstrained fn dirty_trailing_element_fails() { + let mut vec = BoundedVec::::from_array([1]); + // We use the unchecked setter to write past the length, knowingly breaking the invariant. + vec.set_unchecked(1, 42); + assert_bounded_vec_trimmed(vec); + } + + #[test(should_fail_with = "BoundedVec has non-zero trailing elements")] + unconstrained fn dirty_last_element_fails() { + let mut vec = BoundedVec::::from_array([1, 2]); + vec.set_unchecked(2, 99); + assert_bounded_vec_trimmed(vec); + } + + #[test] + unconstrained fn trimmed_array_elements() { + // Test with array element type (like get_notes_oracle returns BoundedVec<[Field; N], MaxNotes>). + let vec = BoundedVec::<[Field; 2], 3>::from_array([[1, 2], [3, 4]]); + assert_bounded_vec_trimmed(vec); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr b/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr index 52bf6c799cc0..05a18cec68c7 100644 --- a/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/utils/array/mod.nr @@ -1,9 +1,11 @@ pub mod append; +pub mod assert_trimmed; pub mod collapse; pub mod subarray; pub mod subbvec; pub use append::append; +pub(crate) use assert_trimmed::assert_bounded_vec_trimmed; pub use collapse::collapse; pub use subarray::subarray; pub use subbvec::subbvec;