diff --git a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr index 6f2553564636..6cc1673f659a 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/mod.nr @@ -1,6 +1,5 @@ -// TODO(#12750): don't make this value assume we're using AES. -use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS; -use dep::protocol_types::{address::AztecAddress, debug_log::debug_log}; +use crate::encrypted_logs::log_encryption::PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS; +use protocol_types::{address::AztecAddress, debug_log::debug_log}; pub mod private_logs; pub mod private_notes; @@ -63,7 +62,7 @@ type ComputeNoteHashAndNullifier = unconstrained fn[Env](/* packed_note */B /// Performs the message discovery process, in which private are downloaded and inspected to find new private notes, /// partial notes and events, etc., and pending partial notes are processed to search for their completion logs. -/// This is the mechanism via which a contract updates its knowldge of its private state. +/// This is the mechanism via which a contract updates its knowledge of its private state. /// /// Receives the address of the contract on which discovery is performed along with its /// `compute_note_hash_and_nullifier` function. diff --git a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr index d1f8d053c40c..aa919e233966 100644 --- a/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr @@ -1,19 +1,24 @@ -use crate::{oracle::message_discovery::sync_notes, utils::array}; +use crate::{ + discovery::{ + ComputeNoteHashAndNullifier, MAX_LOG_CONTENT_LEN, + partial_notes::process_partial_note_private_log, PRIVATE_LOG_EXPANDED_METADATA_LEN, + private_notes::process_private_note_log, + }, + encrypted_logs::{ + log_assembly_strategies::default_aes128::aes128::AES128, log_encryption::LogEncryption, + }, + oracle::message_discovery::sync_notes, + utils::array, +}; -use dep::protocol_types::{ +use protocol_types::{ address::AztecAddress, constants::{MAX_NOTE_HASHES_PER_TX, PRIVATE_LOG_SIZE_IN_FIELDS}, debug_log::{debug_log, debug_log_format}, }; -use crate::discovery::{ - ComputeNoteHashAndNullifier, MAX_LOG_CONTENT_LEN, - partial_notes::process_partial_note_private_log, PRIVATE_LOG_EXPANDED_METADATA_LEN, - private_notes::process_private_note_log, -}; -use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::decrypt_log; -// TODO(#12750): don't make this value assume we're using AES. -use crate::encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS; +// TODO(#12750): don't make these values assume we're using AES. +use crate::encrypted_logs::log_encryption::PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS; /// Searches for private logs that signal new private notes that are then delivered to PXE, or new partial notes that /// are stored in the PXE capsules so that `fetch_and_process_public_partial_note_completion_logs` can later search for @@ -49,7 +54,10 @@ pub unconstrained fn do_process_log( ) { debug_log_format("Processing log with tag {0}", [log.get(0)]); - let log_plaintext = decrypt_log(log, recipient); + // The tag is ignored for now. + let ciphertext = array::subbvec(log, 1); + + let log_plaintext = AES128::decrypt_log(ciphertext, recipient); // The first thing to do after decrypting the log is to determine what type of private log we're processing. We // currently just have two log types: 0 for private notes and 1 for partial notes. This will likely be expanded and @@ -90,7 +98,7 @@ pub unconstrained fn do_process_log( /// - an arbitrary number of fields following that called the 'log content' /// /// ``` -/// log_plainext: [ log_expanded_metadata, ...log_content ] +/// log_plaintext: [ log_expanded_metadata, ...log_content ] /// ``` /// /// The expanded metadata itself is (currently) interpreted as a u64, of which: diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/aes128.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/aes128.nr new file mode 100644 index 000000000000..fda55d8e3730 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/aes128.nr @@ -0,0 +1,302 @@ +use crate::{ + encrypted_logs::{ + encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, + log_assembly_strategies::default_aes128::arithmetic_generics_utils::{ + get_arr_of_size__log_bytes__from_PT, get_arr_of_size__log_bytes_padding__from_PT, + }, + log_encryption::{ + EPH_PK_SIGN_BYTE_SIZE_IN_BYTES, EPH_PK_X_SIZE_IN_FIELDS, + HEADER_CIPHERTEXT_SIZE_IN_BYTES, LogEncryption, PRIVATE_LOG_CIPHERTEXT_LEN, + PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS, + }, + }, + keys::{ + ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, + ephemeral::generate_ephemeral_key_pair, + }, + oracle::{aes128_decrypt::aes128_decrypt_oracle, shared_secret::get_shared_secret}, + prelude::AztecAddress, + utils::{ + array, + conversion::{ + bytes_to_fields::{bytes_from_fields, bytes_to_fields}, + fields_to_bytes::{fields_from_bytes, fields_to_bytes}, + }, + point::{get_sign_of_point, point_from_x_coord_and_sign}, + random::get_random_bytes, + }, +}; +use std::aes128::aes128_encrypt; + +pub struct AES128 {} + +impl LogEncryption for AES128 { + fn encrypt_log( + plaintext: [Field; PLAINTEXT_LEN], + recipient: AztecAddress, + ) -> [Field; PRIVATE_LOG_CIPHERTEXT_LEN] { + // AES 128 operates on bytes, not fields, so we need to convert the fields to bytes. + // (This process is then reversed when processing the log in `do_process_log`) + let plaintext_bytes = fields_to_bytes(plaintext); + + // ***************************************************************************** + // Compute the shared secret + // ***************************************************************************** + + let (eph_sk, eph_pk) = generate_ephemeral_key_pair(); + + let eph_pk_sign_byte: u8 = get_sign_of_point(eph_pk) as u8; + + // (not to be confused with the tagging shared secret) + let ciphertext_shared_secret = + derive_ecdh_shared_secret_using_aztec_address(eph_sk, recipient); + + // TODO: also use this shared secret for deriving note randomness. + + // ***************************************************************************** + // Convert the plaintext into whatever format the encryption function expects + // ***************************************************************************** + + // Already done for this strategy: AES expects bytes. + + // ***************************************************************************** + // Encrypt the plaintext + // ***************************************************************************** + + let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( + ciphertext_shared_secret, + ); + + let ciphertext_bytes = aes128_encrypt(plaintext_bytes, iv, sym_key); + + // |full_pt| = |pt_length| + |pt| + // |pt_aes_padding| = 16 - (|full_pt| % 16) + // or... since a % b is the same as a - b * (a // b) (integer division), so: + // |pt_aes_padding| = 16 - (|full_pt| - 16 * (|full_pt| // 16)) + // |ct| = |full_pt| + |pt_aes_padding| + // = |full_pt| + 16 - (|full_pt| - 16 * (|full_pt| // 16)) + // = 16 + 16 * (|full_pt| // 16) + // = 16 * (1 + |full_pt| // 16) + assert(ciphertext_bytes.len() == 16 * (1 + (PLAINTEXT_LEN * 32) / 16)); + + // ***************************************************************************** + // Compute the header ciphertext + // ***************************************************************************** + + // Header contains only the length of the ciphertext stored in 2 bytes. + // TODO: consider nuking the header altogether and just have a fixed-size ciphertext by padding the plaintext. + // This would be more costly constraint-wise but cheaper DA-wise. + let mut header_plaintext: [u8; 2] = [0 as u8; 2]; + let ciphertext_bytes_length = ciphertext_bytes.len(); + header_plaintext[0] = (ciphertext_bytes_length >> 8) as u8; + header_plaintext[1] = ciphertext_bytes_length as u8; + + // TODO: this is insecure and wasteful: + // "Insecure", because the esk shouldn't be used twice (once for the header, + // and again for the proper ciphertext) (at least, I never got the + // "go ahead" that this would be safe, unfortunately). + // "Wasteful", because the exact same computation is happening further down. + // I'm leaving that 2nd computation where it is, because this 1st computation + // will be imminently deleted, when the header logic is deleted. + let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( + ciphertext_shared_secret, + ); + + // Note: the aes128_encrypt builtin fn automatically appends bytes to the + // input, according to pkcs#7; hence why the output `header_ciphertext_bytes` is 16 + // bytes larger than the input in this case. + let header_ciphertext_bytes = aes128_encrypt(header_plaintext, iv, sym_key); + // I recall that converting a slice to an array incurs constraints, so I'll check the length this way instead: + assert(header_ciphertext_bytes.len() == HEADER_CIPHERTEXT_SIZE_IN_BYTES); + + // ***************************************************************************** + // Prepend / append more bytes of data to the ciphertext, before converting back + // to fields. + // ***************************************************************************** + + let mut log_bytes_padding_to_mult_31 = + get_arr_of_size__log_bytes_padding__from_PT::(); + // Safety: this randomness won't be constrained to be random. It's in the + // interest of the executor of this fn to encrypt with random bytes. + log_bytes_padding_to_mult_31 = unsafe { get_random_bytes() }; + + let mut log_bytes = get_arr_of_size__log_bytes__from_PT::(); + + assert( + log_bytes.len() % 31 == 0, + "Unexpected error: log_bytes.len() should be divisible by 31, by construction.", + ); + + log_bytes[0] = eph_pk_sign_byte; + let mut offset = 1; + for i in 0..header_ciphertext_bytes.len() { + log_bytes[offset + i] = header_ciphertext_bytes[i]; + } + offset += header_ciphertext_bytes.len(); + + for i in 0..ciphertext_bytes.len() { + log_bytes[offset + i] = ciphertext_bytes[i]; + } + offset += ciphertext_bytes.len(); + + for i in 0..log_bytes_padding_to_mult_31.len() { + log_bytes[offset + i] = log_bytes_padding_to_mult_31[i]; + } + + assert( + offset + log_bytes_padding_to_mult_31.len() == log_bytes.len(), + "Something has gone wrong", + ); + + // ***************************************************************************** + // Convert bytes back to fields + // ***************************************************************************** + + // TODO(#12749): As Mike pointed out, we need to make logs produced by different encryption schemes + // indistinguishable from each other and for this reason the output here and in the last for-loop of this function + // should cover a full field. + let log_bytes_as_fields = bytes_to_fields(log_bytes); + + // ***************************************************************************** + // Prepend / append fields, to create the final log + // ***************************************************************************** + + let mut ciphertext: [Field; PRIVATE_LOG_CIPHERTEXT_LEN] = [0; PRIVATE_LOG_CIPHERTEXT_LEN]; + + ciphertext[0] = eph_pk.x; + + let mut offset = 1; + for i in 0..log_bytes_as_fields.len() { + ciphertext[offset + i] = log_bytes_as_fields[i]; + } + offset += log_bytes_as_fields.len(); + + for i in offset..PRIVATE_LOG_CIPHERTEXT_LEN { + // We need to get a random value that fits in 31 bytes to not leak information about the size of the log + // (all the "real" log fields contain at most 31 bytes because of the way we convert the bytes to fields). + // TODO(#12749): Long term, this is not a good solution. + + // Safety: we assume that the sender wants for the log to be private - a malicious one could simply reveal its + // contents publicly. It is therefore fine to trust the sender to provide random padding. + let field_bytes = unsafe { get_random_bytes::<31>() }; + ciphertext[i] = Field::from_be_bytes::<31>(field_bytes); + } + + ciphertext + } + + unconstrained fn decrypt_log( + ciphertext: BoundedVec, + recipient: AztecAddress, + ) -> BoundedVec { + let eph_pk_x = ciphertext.get(0); + + let ciphertext_without_eph_pk_x_fields = array::subbvec::( + ciphertext, + EPH_PK_X_SIZE_IN_FIELDS, + ); + + // Convert the ciphertext represented as fields to a byte representation (its original format) + let ciphertext_without_eph_pk_x = bytes_from_fields(ciphertext_without_eph_pk_x_fields); + + // First byte of the ciphertext represents the ephemeral public key sign + let eph_pk_sign_bool = ciphertext_without_eph_pk_x.get(0) as bool; + // With the sign and the x-coordinate of the ephemeral public key, we can reconstruct the point + let eph_pk = point_from_x_coord_and_sign(eph_pk_x, eph_pk_sign_bool); + + // Derive shared secret and symmetric key + let ciphertext_shared_secret = get_shared_secret(recipient, eph_pk); + let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( + ciphertext_shared_secret, + ); + + // Extract the header ciphertext + let header_start = EPH_PK_SIGN_BYTE_SIZE_IN_BYTES; // Skip eph_pk_sign byte + let header_ciphertext: [u8; HEADER_CIPHERTEXT_SIZE_IN_BYTES] = + array::subarray(ciphertext_without_eph_pk_x.storage(), header_start); + // We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's designed to work + // with logs with unknown length at compile time. This would not be necessary here as the header ciphertext length + // is fixed. But we do it anyway to not have to have duplicate oracles. + let header_ciphertext_bvec = + BoundedVec::::from_array(header_ciphertext); + + // Decrypt header + let header_plaintext = aes128_decrypt_oracle(header_ciphertext_bvec, iv, sym_key); + + // Extract ciphertext length from header (2 bytes, big-endian) + let ciphertext_length = + ((header_plaintext.get(0) as u32) << 8) | (header_plaintext.get(1) as u32); + + // Extract and decrypt main ciphertext + let ciphertext_start = header_start + HEADER_CIPHERTEXT_SIZE_IN_BYTES; + let ciphertext_with_padding: [u8; (PRIVATE_LOG_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS) * 31 - HEADER_CIPHERTEXT_SIZE_IN_BYTES - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES] = + array::subarray(ciphertext_without_eph_pk_x.storage(), ciphertext_start); + let ciphertext: BoundedVec = + BoundedVec::from_parts(ciphertext_with_padding, ciphertext_length); + + // Decrypt main ciphertext and return it + let plaintext_bytes = aes128_decrypt_oracle(ciphertext, iv, sym_key); + + // Each field of the original note log was serialized to 32 bytes so we convert the bytes back to fields. + fields_from_bytes(plaintext_bytes) + } +} + +mod test { + use crate::{ + encrypted_logs::log_encryption::{LogEncryption, PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS}, + keys::ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, + test::helpers::test_environment::TestEnvironment, + }; + use super::AES128; + use protocol_types::{address::AztecAddress, traits::FromField}; + use std::{embedded_curve_ops::EmbeddedCurveScalar, test::OracleMock}; + + #[test] + unconstrained fn encrypt_decrypt_log() { + let mut env = TestEnvironment::new(); + // Advance 1 block so we can read historic state from private + env.advance_block_by(1); + + let plaintext = [1, 2, 3]; + + let recipient = AztecAddress::from_field( + 0x25afb798ea6d0b8c1618e50fdeafa463059415013d3b7c75d46abf5e242be70c, + ); + + // Mock random values for deterministic test + let eph_sk = 0x1358d15019d4639393d62b97e1588c095957ce74a1c32d6ec7d62fe6705d9538; + let _ = OracleMock::mock("getRandomField").returns(eph_sk).times(1); + + let randomness = 0x0101010101010101010101010101010101010101010101010101010101010101; + let _ = OracleMock::mock("getRandomField").returns(randomness).times(1000000); + + let _ = OracleMock::mock("getIndexedTaggingSecretAsSender").returns([69420, 1337]); + let _ = OracleMock::mock("incrementAppTaggingSecretIndexAsSender").returns(()); + + // Encrypt the log + let encrypted_log = BoundedVec::from_array(AES128::encrypt_log(plaintext, recipient)); + + // Mock shared secret for deterministic test + let shared_secret = derive_ecdh_shared_secret_using_aztec_address( + EmbeddedCurveScalar::from_field(eph_sk), + recipient, + ); + let _ = OracleMock::mock("getSharedSecret").returns(shared_secret); + + // Decrypt the log + let decrypted = AES128::decrypt_log(encrypted_log, recipient); + + // The decryption function spits out a BoundedVec because it's designed to work with logs with unknown length + // at compile time. For this reason we need to convert the original input to a BoundedVec. + let plaintext_bvec = + BoundedVec::::from_array(plaintext); + + // Verify decryption matches original plaintext + assert_eq(decrypted, plaintext_bvec, "Decrypted bytes should match original plaintext"); + + // The following is a workaround of "struct is never constructed" Noir compilation error (we only ever use + // static methods of the struct). + let _ = AES128 {}; + } +} diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/arithmetic_generics_utils.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/arithmetic_generics_utils.nr index 6d3c4b05e983..d5776acf0be9 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/arithmetic_generics_utils.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/arithmetic_generics_utils.nr @@ -1,10 +1,12 @@ +use crate::encrypted_logs::log_encryption::HEADER_CIPHERTEXT_SIZE_IN_BYTES; + /********************************************************/ // Disgusting arithmetic on generics /********************************************************/ // In this section, instead of initialising arrays with very complicated generic // arithmetic, such as: -// let my_arr: [u8; (((PT + (16 - (PT % 16))) + 49) + ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49)))] = [0; (((PT + (16 - (PT % 16))) + 49) + ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49)))]; +// let my_arr: [u8; (((PT + (16 - (PT % 16))) + (HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1)) + ((((((PT + (16 - (PT % 16))) + (HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1)) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + (HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1))))] = [0; (((PT + (16 - (PT % 16))) + (HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1)) + ((((((PT + (16 - (PT % 16))) + (HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1)) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + (HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1))))]; //... we instead do the arithmetic a little bit at a time, so that the computation // can be audited and understood. Now, we can't do arithmetic on generics in the body // of a function, so we abusing functions in the following way: @@ -31,13 +33,15 @@ fn get_arr_of_size__ciphertext( // Ok, so we have the following bytes: // eph_pk_sign, header_ciphertext, ciphertext: -// Let lbwop = 1 + 48 + |ct| // aka log bytes without padding -fn get_arr_of_size__log_bytes_without_padding(_ct: [u8; CT]) -> [u8; 1 + 48 + CT] { - [0; 1 + 48 + CT] +// Let lbwop = 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| // aka log bytes without padding +fn get_arr_of_size__log_bytes_without_padding( + _ct: [u8; CT], +) -> [u8; 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + CT] { + [0; 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + CT] } // Recall: -// lbwop := 1 + 48 + |ct| // aka log bytes without padding +// lbwop := 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| // aka log bytes without padding // We now want to pad b to the next multiple of 31, so as to "fill" fields. // Let p be that padding. // p = 31 * ceil(lbwop / 31) - lbwop @@ -49,9 +53,9 @@ fn get_arr_of_size__log_bytes_padding( [0; (31 * ((LBWOP + 30) / 31)) - LBWOP] } -// |log_bytes| = 1 + 48 + |ct| + p // aka log bytes (with padding) +// |log_bytes| = 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| + p // aka log bytes (with padding) // Recall: -// lbwop := 1 + 48 + |ct| +// lbwop := 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| // p is the padding fn get_arr_of_size__log_bytes( _lbwop: [u8; LBWOP], @@ -63,7 +67,7 @@ fn get_arr_of_size__log_bytes( // The return type is pasted from the LSP's expectation, because it was too difficult // to match its weird way of doing algebra. It doesn't know all rules of arithmetic. // PT is the plaintext length. -pub(crate) fn get_arr_of_size__log_bytes_padding__from_PT() -> [u8; ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49))] { +pub(crate) fn get_arr_of_size__log_bytes_padding__from_PT() -> [u8; ((((((PT + (16 - (PT % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1))] { let full_pt = get_arr_of_size__full_plaintext::(); let pt_aes_padding = get_arr_of_size__plaintext_aes_padding(full_pt); let ct = get_arr_of_size__ciphertext(full_pt, pt_aes_padding); @@ -74,7 +78,7 @@ pub(crate) fn get_arr_of_size__log_bytes_padding__from_PT() -> [u8; // The return type is pasted from the LSP's expectation, because it was too difficult // to match its weird way of doing algebra. It doesn't know all rules of arithmetic. -pub(crate) fn get_arr_of_size__log_bytes__from_PT() -> [u8; (((PT + (16 - (PT % 16))) + 49) + ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49)))] { +pub(crate) fn get_arr_of_size__log_bytes__from_PT() -> [u8; (((PT + (16 - (PT % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1) + ((((((PT + (16 - (PT % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1)))] { let full_pt = get_arr_of_size__full_plaintext::(); let pt_aes_padding = get_arr_of_size__plaintext_aes_padding(full_pt); let ct = get_arr_of_size__ciphertext(full_pt, pt_aes_padding); diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/event.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/event.nr index f903b1598f31..2527164230b4 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/event.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/event.nr @@ -1,20 +1,11 @@ use crate::{ context::PrivateContext, - encrypted_logs::{ - encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, - log_assembly_strategies::default_aes128::{ - arithmetic_generics_utils::{ - get_arr_of_size__log_bytes__from_PT, get_arr_of_size__log_bytes_padding__from_PT, - }, - note::encryption::HEADER_CIPHERTEXT_SIZE_IN_BYTES, - }, - }, + encrypted_logs::encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, event::event_interface::EventInterface, keys::{ ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, ephemeral::generate_ephemeral_key_pair, }, - oracle::notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, utils::{ conversion::{bytes_to_fields::bytes_to_fields, fields_to_bytes::fields_to_bytes}, point::get_sign_of_point, @@ -28,6 +19,8 @@ use dep::protocol_types::{ }; use std::aes128::aes128_encrypt; +global HEADER_CIPHERTEXT_SIZE_IN_BYTES: u32 = 48; + /* * WHY IS THERE LOTS OF CODE DUPLICATION BETWEEN event.nr and note.nr? * It's because there are a few more optimisations that can be done for notes, @@ -87,7 +80,7 @@ fn compute_log( context: PrivateContext, event: Event, recipient: AztecAddress, - sender: AztecAddress, + _sender: AztecAddress, ) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] where Event: EventInterface, @@ -219,15 +212,12 @@ where // In this strategy, we prepend [tag, eph_pk.x] - // Safety: we assume that the sender wants for the recipient to find the tagged note, and therefore that they will - // cooperate and use the correct tag. Usage of a bad tag will result in the recipient not being able to find the - // note automatically. - let tag = unsafe { get_app_tag_as_sender(sender, recipient) }; - increment_app_tagging_secret_index_as_sender(sender, recipient); - let mut final_log: [Field; PRIVATE_LOG_SIZE_IN_FIELDS] = [0; PRIVATE_LOG_SIZE_IN_FIELDS]; - final_log[0] = tag; + // At this point we feed in an incorrect tag because we don't want the event logs to be processed when invoking + // `sync_notes`. This currently fine as the events are solely to be obtain via the getPrivateEvents function + // that doesn't work with the tag. + final_log[0] = 1234; final_log[1] = eph_pk.x; let mut offset = 2; @@ -297,3 +287,92 @@ where context.emit_private_log(encrypted_log); } } + +// These utils got copied here because note encryption diverged from the event one. The encryption functionality +// +// in this file is soon to be removed and replaced with the standard AES128 encryption from ./aes128.nr. +/********************************************************/ +// Disgusting arithmetic on generics +/********************************************************/ + +// In this section, instead of initialising arrays with very complicated generic +// arithmetic, such as: +// let my_arr: [u8; (((PT + (16 - (PT % 16))) + 49) + ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49)))] = [0; (((PT + (16 - (PT % 16))) + 49) + ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49)))]; +//... we instead do the arithmetic a little bit at a time, so that the computation +// can be audited and understood. Now, we can't do arithmetic on generics in the body +// of a function, so we abusing functions in the following way: + +// |full_pt| = |pt| = (N * 32) + 64 +fn get_arr_of_size__full_plaintext() -> [u8; PT] { + [0; PT] +} + +// |pt_aes_padding| = 16 - (|full_pt| % 16) +fn get_arr_of_size__plaintext_aes_padding( + _full_pt: [u8; FULL_PT], +) -> [u8; 16 - (FULL_PT % 16)] { + [0; 16 - (FULL_PT % 16)] +} + +// |ct| = |full_pt| + |pt_aes_padding| +fn get_arr_of_size__ciphertext( + _full_pt: [u8; FULL_PT], + _pt_aes_padding: [u8; PT_AES_PADDING], +) -> [u8; FULL_PT + PT_AES_PADDING] { + [0; FULL_PT + PT_AES_PADDING] +} + +// Ok, so we have the following bytes: +// eph_pk_sign, header_ciphertext, ciphertext: +// Let lbwop = 1 + 48 + |ct| // aka log bytes without padding +fn get_arr_of_size__log_bytes_without_padding(_ct: [u8; CT]) -> [u8; 1 + 48 + CT] { + [0; 1 + 48 + CT] +} + +// Recall: +// lbwop := 1 + 48 + |ct| // aka log bytes without padding +// We now want to pad b to the next multiple of 31, so as to "fill" fields. +// Let p be that padding. +// p = 31 * ceil(lbwop / 31) - lbwop +// = 31 * ((lbwop + 30) // 31) - lbwop +// (because ceil(x / y) = (x + y - 1) // y ). +fn get_arr_of_size__log_bytes_padding( + _lbwop: [u8; LBWOP], +) -> [u8; (31 * ((LBWOP + 30) / 31)) - LBWOP] { + [0; (31 * ((LBWOP + 30) / 31)) - LBWOP] +} + +// |log_bytes| = 1 + 48 + |ct| + p // aka log bytes (with padding) +// Recall: +// lbwop := 1 + 48 + |ct| +// p is the padding +fn get_arr_of_size__log_bytes( + _lbwop: [u8; LBWOP], + _p: [u8; P], +) -> [u8; LBWOP + P] { + [0; LBWOP + P] +} + +// The return type is pasted from the LSP's expectation, because it was too difficult +// to match its weird way of doing algebra. It doesn't know all rules of arithmetic. +// PT is the plaintext length. +pub(crate) fn get_arr_of_size__log_bytes_padding__from_PT() -> [u8; ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49))] { + let full_pt = get_arr_of_size__full_plaintext::(); + let pt_aes_padding = get_arr_of_size__plaintext_aes_padding(full_pt); + let ct = get_arr_of_size__ciphertext(full_pt, pt_aes_padding); + let lbwop = get_arr_of_size__log_bytes_without_padding(ct); + let p = get_arr_of_size__log_bytes_padding(lbwop); + p +} + +// The return type is pasted from the LSP's expectation, because it was too difficult +// to match its weird way of doing algebra. It doesn't know all rules of arithmetic. +pub(crate) fn get_arr_of_size__log_bytes__from_PT() -> [u8; (((PT + (16 - (PT % 16))) + 49) + ((((((PT + (16 - (PT % 16))) + 49) + 30) / 31) * 31) - ((PT + (16 - (PT % 16))) + 49)))] { + let full_pt = get_arr_of_size__full_plaintext::(); + let pt_aes_padding = get_arr_of_size__plaintext_aes_padding(full_pt); + let ct = get_arr_of_size__ciphertext(full_pt, pt_aes_padding); + let lbwop = get_arr_of_size__log_bytes_without_padding(ct); + let p = get_arr_of_size__log_bytes_padding(lbwop); + let log_bytes = get_arr_of_size__log_bytes(lbwop, p); + log_bytes +} diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/mod.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/mod.nr index eeda21a1875d..4b1e58b3028f 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/mod.nr @@ -1,3 +1,4 @@ +pub mod aes128; pub mod arithmetic_generics_utils; pub mod event; pub mod note; diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr similarity index 55% rename from noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr rename to noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr index 2c46d6f0922a..eacfca5f0f36 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/note.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note.nr @@ -1,108 +1,16 @@ use crate::{ context::PrivateContext, - encrypted_logs::log_assembly_strategies::default_aes128::note::encryption::encrypt_log, + encrypted_logs::{ + log_assembly_strategies::default_aes128::aes128::AES128, log_encryption::LogEncryption, + }, note::{note_emission::NoteEmission, note_interface::NoteType}, + oracle::notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, }; use dep::protocol_types::{ abis::note_hash::NoteHash, address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, traits::Packable, }; -/* - * WHY IS THERE LOTS OF CODE DUPLICATION BETWEEN event.nr and note.nr? - * It's because there are a few more optimisations that can be done for notes, - * and so the stuff that looks like duplicated code currently, won't be - * the same for long. - * To modularise now feels premature, because we might get that modularisation wrong. - * Much better (imo) to have a flattened templates for log assembly, because it - * makes it much easier for us all to follow, it serves as a nice example for the - * community to follow (if they wish to roll their own log layouts), and it gives - * us more time to spot common patterns across all kinds of log layouts. - */ - -/* - * LOG CONFIGURATION CHOICES: - * - * deliver_to: INPUT as recipient: AztecAddress - * encrypt_with: aes128 CBC (Cipher Block Chaining) - * shared_secret: ephemeral - * shared_secret_randomness_extraction_hash: sha256 - * tag: true - * tag_from: INPUT as sender: AztecAddress - * - * Note-specific: - * derive_note_randomness_from_shared_secret: false - * - */ - -/* - * LOG LAYOUT CHOICE: - * - * Short explanation: - * log = [tag, epk, header_ct=[[contract_address, ct_len], pkcs7_pad], ct=[[pt], pkcs7_pad], some bytes padding, some fields padding] - * - * Long explanation: - * tag: Field - * epk: [Field, u8] - * header_ct: [[u8; 32], [u8; 2], [u8; 16]] - * ct: [[u8; 2], [u8; x], [u8; y]] - * - * More precisely (in pseudocode): - * - * log = [ - * tag: Field, - * Epk: Field, - * - * le_bytes_31_to_fields( - * - * log_bytes: [ - * eph_pk_sign: [u8; 1], - * - * header_ciphertext: aes_encrypt( - * contract_address: [u8; 32], - * ct_length: [u8; 2], - * - * // the aes128_encrypt fn automatically inserts padding: - * header_pt_aes_padding: [u8; 14], // `16 - (input.len() % 16)` - - * ): [u8; 48], - * - * ciphertext: aes_encrypt( - * final_pt: [ - * pt: { - * note_bytes: { - * storage_slot: [u8; 32], - * note_type_id: [u8; 32], - * ...note: [u8; N * 32], - * }: [u8; N * 32 + 64], - * }: [u8; N * 32 + 64], - - * ]: [u8; N * 32 + 64], - * - * // the aes128_encrypt fn automatically inserts padding: - * pt_aes_padding: [u8; 16 - ( (|pt_length| + |pt|) % 16 )] - * - * ): [u8; |pt| + |pt_aes_padding|] - * [u8; |ct|] - * - * log_bytes_padding_to_mult_31: [u8; 31 * ceil((1 + 48 + |ct|)/31) - (1 + 48 + |ct|)], - * [u8; p] - * - * ]: [u8; 1 + 48 + |ct| + p] - * - * ): [Field; (1 + 48 + |ct| + p) / 31] - * - * log_fields_padding: [Field; PRIVATE_LOG_SIZE_IN_FIELDS - 2 - (1 + 48 + |ct| + p) / 31], - * - * ]: [Field; PRIVATE_LOG_SIZE_IN_FIELDS] - * - * - */ - -/********************************************************/ -// End of disgusting arithmetic on generics -/********************************************************/ - // TODO: it feels like this existence check is in the wrong place. In fact, why is it needed at all? Under what circumstances have we found a non-existent note being emitted accidentally? fn assert_note_exists(context: PrivateContext, note_hash_counter: u32) { // TODO(#8589): use typesystem to skip this check when not needed @@ -143,7 +51,6 @@ where } pub fn compute_log( - context: PrivateContext, note: Note, storage_slot: Field, recipient: AztecAddress, @@ -152,13 +59,37 @@ pub fn compute_log( where Note: NoteType + Packable, { - let final_plaintext = compute_note_plaintext_for_this_strategy(note, storage_slot); + let plaintext = compute_note_plaintext_for_this_strategy(note, storage_slot); + + let ciphertext = AES128::encrypt_log(plaintext, recipient); + + let log = prefix_with_tag(ciphertext, sender, recipient); + + log +} - encrypt_log(context.this_address(), final_plaintext, recipient, sender) +fn prefix_with_tag( + log_without_tag: [Field; L], + sender: AztecAddress, + recipient: AztecAddress, +) -> [Field; L + 1] { + // Safety: we assume that the sender wants for the recipient to find the tagged note, and therefore that they will + // cooperate and use the correct tag. Usage of a bad tag will result in the recipient not being able to find the + // note automatically. + let tag = unsafe { get_app_tag_as_sender(sender, recipient) }; + increment_app_tagging_secret_index_as_sender(sender, recipient); + + let mut log_with_tag = [0; L + 1]; + + log_with_tag[0] = tag; + for i in 0..log_without_tag.len() { + log_with_tag[i + 1] = log_without_tag[i]; + } + + log_with_tag } pub unconstrained fn compute_log_unconstrained( - context: PrivateContext, note: Note, storage_slot: Field, recipient: AztecAddress, @@ -167,7 +98,7 @@ pub unconstrained fn compute_log_unconstrained( where Note: NoteType + Packable, { - compute_log(context, note, storage_slot, recipient, sender) + compute_log(note, storage_slot, recipient, sender) } /// Sends an encrypted message to `recipient` with the content of the note, which they will discover when processing @@ -187,7 +118,7 @@ where let note_hash_counter = e.note_hash_counter; assert_note_exists(*context, note_hash_counter); - let encrypted_log = compute_log(*context, note, storage_slot, recipient, sender); + let encrypted_log = compute_log(note, storage_slot, recipient, sender); context.emit_raw_note_log(encrypted_log, note_hash_counter); } } @@ -213,9 +144,45 @@ where assert_note_exists(*context, note_hash_counter); - // Safety: this function does not constrain the encryption of the log, as explainted on its description. + // Safety: this function does not constrain the encryption of the log, as explained on its description. let encrypted_log = - unsafe { compute_log_unconstrained(*context, note, storage_slot, recipient, sender) }; + unsafe { compute_log_unconstrained(note, storage_slot, recipient, sender) }; context.emit_raw_note_log(encrypted_log, note_hash_counter); } } + +mod test { + use super::prefix_with_tag; + use protocol_types::{ + address::AztecAddress, + indexed_tagging_secret::IndexedTaggingSecret, + traits::{Deserialize, FromField, Serialize}, + }; + use std::test::OracleMock; + + #[test] + unconstrained fn prefixing_with_tag() { + let sender = AztecAddress::from_field(1); + let recipient = AztecAddress::from_field(2); + + let app_tagging_secret = 42; + let index = 5; + + // I am using the deserialize trait instead of directly instantiating the IndexedTaggingSecret struct because + // direct instantiation functionality is not exposed. + let indexed_tagging_secret = IndexedTaggingSecret::deserialize([app_tagging_secret, index]); + + // Mock the tagging oracles + let _ = OracleMock::mock("getIndexedTaggingSecretAsSender").returns(indexed_tagging_secret + .serialize()); + let _ = OracleMock::mock("incrementAppTaggingSecretIndexAsSender").returns(()); + + let log_without_tag = [1, 2, 3]; + let log_with_tag = prefix_with_tag(log_without_tag, sender, recipient); + + let expected_result = [indexed_tagging_secret.compute_tag(recipient), 1, 2, 3]; + + // Check tag was prefixed correctly + assert_eq(log_with_tag, expected_result, "Tag was not prefixed correctly"); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr deleted file mode 100644 index c22047499739..000000000000 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/encryption.nr +++ /dev/null @@ -1,360 +0,0 @@ -use crate::{ - encrypted_logs::{ - encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256, - log_assembly_strategies::default_aes128::arithmetic_generics_utils::{ - get_arr_of_size__log_bytes__from_PT, get_arr_of_size__log_bytes_padding__from_PT, - }, - }, - keys::{ - ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, - ephemeral::generate_ephemeral_key_pair, - }, - oracle::{ - aes128_decrypt::aes128_decrypt_oracle, - notes::{get_app_tag_as_sender, increment_app_tagging_secret_index_as_sender}, - shared_secret::get_shared_secret, - }, - utils::{ - array, - conversion::{ - bytes_to_fields::{bytes_from_fields, bytes_to_fields}, - fields_to_bytes::{fields_from_bytes, fields_to_bytes}, - }, - point::{get_sign_of_point, point_from_x_coord_and_sign}, - random::get_random_bytes, - }, -}; -use protocol_types::{address::AztecAddress, constants::PRIVATE_LOG_SIZE_IN_FIELDS, traits::ToField}; -use std::aes128::aes128_encrypt; - -// contract_address (32) + ciphertext_length (2) + 16 bytes pkcs#7 AES padding. -pub(crate) global HEADER_CIPHERTEXT_SIZE_IN_BYTES: u32 = 48; - -global TAG_AND_EPH_PK_X_SIZE_IN_FIELDS: u32 = 2; -global EPH_PK_SIGN_BYTE_SIZE_IN_BYTES: u32 = 1; -pub global PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES: u32 = ( - PRIVATE_LOG_SIZE_IN_FIELDS - TAG_AND_EPH_PK_X_SIZE_IN_FIELDS -) - * 31 - - HEADER_CIPHERTEXT_SIZE_IN_BYTES - - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES; -// Each field of the original note log was serialized to 32 bytes. Below we convert the bytes back to fields. -pub global PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS: u32 = PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES / 32; - -/// Computes an encrypted log using AES-128 encryption in CBC mode. -/// -/// The resulting log has the following format: -/// ```text -/// [ -/// tag: Field, // Tag for message discovery, derived from sender/recipient -/// epk_x: Field, // X coordinate of ephemeral public key -/// log_bytes: [Field], // Encrypted data converted from bytes to fields, containing: -/// [ -/// epk_sign: u8, // Sign bit of ephemeral public key Y coordinate -/// header_ciphertext: [u8], // AES encrypted header containing: -/// [ -/// contract_address: [u8; 32], // Contract address that emitted the note -/// ciphertext_length: [u8; 2], // Length of main ciphertext in bytes -/// padding: [u8; 14] // PKCS#7 padding to AES block size -/// ], -/// ciphertext: [u8], // AES encrypted note data containing: -/// [ -/// plaintext_bytes: [u8], // The plaintext -/// padding: [u8] // PKCS#7 padding to AES block size -/// ], -/// padding: [u8] // Random padding to make log_bytes multiple of 31 -/// ], -/// padding: [Field] // Random padding to PRIVATE_LOG_SIZE_IN_FIELDS -/// ] -/// ``` -/// -/// The encryption process: -/// 1. Generate ephemeral key-pair and ECDH shared secret with recipient -/// 2. Derive AES key and IV from shared secret using SHA-256 -/// 3. Encrypt header and note data separately using AES-128-CBC -/// 4. Format into final log structure with padding -pub fn encrypt_log( - contract_address: AztecAddress, - plaintext: [Field; PT], - recipient: AztecAddress, - sender: AztecAddress, -) -> [Field; PRIVATE_LOG_SIZE_IN_FIELDS] { - // AES 128 operates on bytes, not fields, so we need to convert the fields to bytes. - // (This process is then reversed when processing the log in `do_process_log`) - let plaintext_bytes = fields_to_bytes(plaintext); - - // ***************************************************************************** - // Compute the shared secret - // ***************************************************************************** - - let (eph_sk, eph_pk) = generate_ephemeral_key_pair(); - - let eph_pk_sign_byte: u8 = get_sign_of_point(eph_pk) as u8; - - // (not to be confused with the tagging shared secret) - let ciphertext_shared_secret = derive_ecdh_shared_secret_using_aztec_address(eph_sk, recipient); - - // TODO: also use this shared secret for deriving note randomness. - - // ***************************************************************************** - // Convert the plaintext into whatever format the encryption function expects - // ***************************************************************************** - - // Already done for this strategy: AES expects bytes. - - // ***************************************************************************** - // Encrypt the plaintext - // ***************************************************************************** - - let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( - ciphertext_shared_secret, - ); - - let ciphertext_bytes = aes128_encrypt(plaintext_bytes, iv, sym_key); - - // |full_pt| = |pt_length| + |pt| - // |pt_aes_padding| = 16 - (|full_pt| % 16) - // or... since a % b is the same as a - b * (a // b) (integer division), so: - // |pt_aes_padding| = 16 - (|full_pt| - 16 * (|full_pt| // 16)) - // |ct| = |full_pt| + |pt_aes_padding| - // = |full_pt| + 16 - (|full_pt| - 16 * (|full_pt| // 16)) - // = 16 + 16 * (|full_pt| // 16) - // = 16 * (1 + |full_pt| // 16) - assert(ciphertext_bytes.len() == 16 * (1 + (PT * 32) / 16)); - - // ***************************************************************************** - // Compute the header ciphertext - // ***************************************************************************** - - let contract_address_bytes = contract_address.to_field().to_be_bytes::<32>(); - - let mut header_plaintext: [u8; 32 + 2] = [0; 32 + 2]; - for i in 0..32 { - header_plaintext[i] = contract_address_bytes[i]; - } - let offset = 32; - let ciphertext_bytes_length = ciphertext_bytes.len(); - header_plaintext[offset] = (ciphertext_bytes_length >> 8) as u8; - header_plaintext[offset + 1] = ciphertext_bytes_length as u8; - - // TODO: this is insecure and wasteful: - // "Insecure", because the esk shouldn't be used twice (once for the header, - // and again for the proper ciphertext) (at least, I never got the - // "go ahead" that this would be safe, unfortunately). - // "Wasteful", because the exact same computation is happening further down. - // I'm leaving that 2nd computation where it is, because this 1st computation - // will be imminently deleted, when the header logic is deleted. - let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( - ciphertext_shared_secret, - ); - - // Note: the aes128_encrypt builtin fn automatically appends bytes to the - // input, according to pkcs#7; hence why the output `header_ciphertext_bytes` is 16 - // bytes larger than the input in this case. - let header_ciphertext_bytes = aes128_encrypt(header_plaintext, iv, sym_key); - // I recall that converting a slice to an array incurs constraints, so I'll check the length this way instead: - assert(header_ciphertext_bytes.len() == HEADER_CIPHERTEXT_SIZE_IN_BYTES); - - // ***************************************************************************** - // Prepend / append more bytes of data to the ciphertext, before converting back - // to fields. - // ***************************************************************************** - - let mut log_bytes_padding_to_mult_31 = get_arr_of_size__log_bytes_padding__from_PT::(); - // Safety: this randomness won't be constrained to be random. It's in the - // interest of the executor of this fn to encrypt with random bytes. - log_bytes_padding_to_mult_31 = unsafe { get_random_bytes() }; - - let mut log_bytes = get_arr_of_size__log_bytes__from_PT::(); - - assert( - log_bytes.len() % 31 == 0, - "Unexpected error: log_bytes.len() should be divisible by 31, by construction.", - ); - - log_bytes[0] = eph_pk_sign_byte; - let mut offset = 1; - for i in 0..header_ciphertext_bytes.len() { - log_bytes[offset + i] = header_ciphertext_bytes[i]; - } - offset += header_ciphertext_bytes.len(); - - for i in 0..ciphertext_bytes.len() { - log_bytes[offset + i] = ciphertext_bytes[i]; - } - offset += ciphertext_bytes.len(); - - for i in 0..log_bytes_padding_to_mult_31.len() { - log_bytes[offset + i] = log_bytes_padding_to_mult_31[i]; - } - - assert( - offset + log_bytes_padding_to_mult_31.len() == log_bytes.len(), - "Something has gone wrong", - ); - - // ***************************************************************************** - // Convert bytes back to fields - // ***************************************************************************** - - // TODO(#12749): As Mike pointed out, we need to make logs produced by different encryption schemes - // indistinguishable from each other and for this reason the output here and in the last for-loop of this function - // should cover a full field. - let log_bytes_as_fields = bytes_to_fields(log_bytes); - - // ***************************************************************************** - // Prepend / append fields, to create the final log - // ***************************************************************************** - - // In this strategy, we prepend [tag, eph_pk.x] - - // Safety: We assume that the sender wants for the recipient to find the tagged note, - // and therefore that they will cooperate and use the correct tag. Usage of a bad - // tag will result in the recipient not being able to find the note automatically. - let tag = unsafe { get_app_tag_as_sender(sender, recipient) }; - increment_app_tagging_secret_index_as_sender(sender, recipient); - - let mut final_log: [Field; PRIVATE_LOG_SIZE_IN_FIELDS] = [0; PRIVATE_LOG_SIZE_IN_FIELDS]; - - final_log[0] = tag; - final_log[1] = eph_pk.x; - - let mut offset = 2; - for i in 0..log_bytes_as_fields.len() { - final_log[offset + i] = log_bytes_as_fields[i]; - } - offset += log_bytes_as_fields.len(); - - for i in offset..PRIVATE_LOG_SIZE_IN_FIELDS { - // We need to get a random value that fits in 31 bytes to not leak information about the size of the log - // (all the "real" log fields contain at most 31 bytes because of the way we convert the bytes to fields). - // TODO(#12749): Long term, this is not a good solution. - - // Safety: we assume that the sender wants for the log to be private - a malicious one could simply reveal its - // contents publicly. It is therefore fine to trust the sender to provide random padding. - let field_bytes = unsafe { get_random_bytes::<31>() }; - final_log[i] = Field::from_be_bytes::<31>(field_bytes); - } - - final_log -} - -pub unconstrained fn decrypt_log( - log: BoundedVec, - recipient: AztecAddress, -) -> BoundedVec { - // let tag = log.get(0); - let eph_pk_x = log.get(1); - - let log_ciphertext_fields = array::subbvec::( - log, - TAG_AND_EPH_PK_X_SIZE_IN_FIELDS, - ); - - // Convert the ciphertext represented as fields to a byte representation (its original format) - let log_ciphertext = bytes_from_fields(log_ciphertext_fields); - - // First byte of the ciphertext represents the ephemeral public key sign - let eph_pk_sign_bool = log_ciphertext.get(0) as bool; - // With the sign and the x-coordinate of the ephemeral public key, we can reconstruct the point - let eph_pk = point_from_x_coord_and_sign(eph_pk_x, eph_pk_sign_bool); - - // Derive shared secret and symmetric key - let ciphertext_shared_secret = get_shared_secret(recipient, eph_pk); - let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256( - ciphertext_shared_secret, - ); - - // Extract the header ciphertext - let header_start = EPH_PK_SIGN_BYTE_SIZE_IN_BYTES; // Skip eph_pk_sign byte - let header_ciphertext: [u8; HEADER_CIPHERTEXT_SIZE_IN_BYTES] = - array::subarray(log_ciphertext.storage(), header_start); - // We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's designed to work - // with logs with unknown length at compile time. This would not be necessary here as the header ciphertext length - // is fixed. But we do it anyway to not have to have duplicate oracles. - let header_ciphertext_bvec = - BoundedVec::::from_array(header_ciphertext); - - // Decrypt header - let header_plaintext = aes128_decrypt_oracle(header_ciphertext_bvec, iv, sym_key); - - // Extract ciphertext length from header (2 bytes, big-endian) - let ciphertext_length = - ((header_plaintext.get(32) as u32) << 8) | (header_plaintext.get(33) as u32); - - // Extract and decrypt main ciphertext - let ciphertext_start = header_start + HEADER_CIPHERTEXT_SIZE_IN_BYTES; - let ciphertext_with_padding: [u8; (PRIVATE_LOG_SIZE_IN_FIELDS - TAG_AND_EPH_PK_X_SIZE_IN_FIELDS) * 31 - HEADER_CIPHERTEXT_SIZE_IN_BYTES - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES] = - array::subarray(log_ciphertext.storage(), ciphertext_start); - let ciphertext: BoundedVec = - BoundedVec::from_parts(ciphertext_with_padding, ciphertext_length); - - // Decrypt main ciphertext and return it - let log_plaintext_bytes = aes128_decrypt_oracle(ciphertext, iv, sym_key); - - // Each field of the original note log was serialized to 32 bytes so we convert the bytes back to fields. - fields_from_bytes(log_plaintext_bytes) -} - -mod test { - use crate::{ - keys::ecdh_shared_secret::derive_ecdh_shared_secret_using_aztec_address, - test::helpers::test_environment::TestEnvironment, - }; - use super::{decrypt_log, encrypt_log, PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS}; - use protocol_types::{address::AztecAddress, traits::FromField}; - use std::{embedded_curve_ops::EmbeddedCurveScalar, test::OracleMock}; - - #[test] - unconstrained fn encrypt_decrypt_log() { - let mut env = TestEnvironment::new(); - // Advance 1 block so we can read historic state from private - env.advance_block_by(1); - - let contract_address = AztecAddress::from_field( - 0x10f48cd9eff7ae5b209c557c70de2e657ee79166868676b787e9417e19260e04, - ); - - let plaintext = [1, 2, 3]; - - let recipient = AztecAddress::from_field( - 0x25afb798ea6d0b8c1618e50fdeafa463059415013d3b7c75d46abf5e242be70c, - ); - - let sender = AztecAddress::from_field( - 0x25afb798ea6d0b8c1618e50fdeafa463059415013d3b7c75d46abf5e242be70c, - ); - - // Mock random values for deterministic test - let eph_sk = 0x1358d15019d4639393d62b97e1588c095957ce74a1c32d6ec7d62fe6705d9538; - let _ = OracleMock::mock("getRandomField").returns(eph_sk).times(1); - - let randomness = 0x0101010101010101010101010101010101010101010101010101010101010101; - let _ = OracleMock::mock("getRandomField").returns(randomness).times(1000000); - - let _ = OracleMock::mock("getIndexedTaggingSecretAsSender").returns([69420, 1337]); - let _ = OracleMock::mock("incrementAppTaggingSecretIndexAsSender").returns(()); - - // Encrypt the log - let encrypted_log = - BoundedVec::from_array(encrypt_log(contract_address, plaintext, recipient, sender)); - - // Mock shared secret for deterministic test - let shared_secret = derive_ecdh_shared_secret_using_aztec_address( - EmbeddedCurveScalar::from_field(eph_sk), - recipient, - ); - let _ = OracleMock::mock("getSharedSecret").returns(shared_secret); - - // Decrypt the log - let decrypted = decrypt_log(encrypted_log, recipient); - - // The decryption function spits out a BoundedVec because it's designed to work with logs with unknown length - // at compile time. For this reason we need to convert the original input to a BoundedVec. - let plaintext_bvec = - BoundedVec::::from_array(plaintext); - - // Verify decryption matches original plaintext - assert_eq(decrypted, plaintext_bvec, "Decrypted bytes should match original plaintext"); - } -} diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/mod.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/mod.nr deleted file mode 100644 index 9aa3d138d205..000000000000 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_assembly_strategies/default_aes128/note/mod.nr +++ /dev/null @@ -1,7 +0,0 @@ -pub mod encryption; -pub mod note; - -pub use note::{ - compute_log, compute_log_unconstrained, encode_and_encrypt_note, - encode_and_encrypt_note_unconstrained, -}; diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_encryption.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_encryption.nr new file mode 100644 index 000000000000..d328dcbf323a --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/log_encryption.nr @@ -0,0 +1,69 @@ +use crate::prelude::AztecAddress; +use protocol_types::constants::PRIVATE_LOG_SIZE_IN_FIELDS; + +pub global PRIVATE_LOG_CIPHERTEXT_LEN: u32 = PRIVATE_LOG_SIZE_IN_FIELDS - 1; // 1 field for the tag + +// TODO(#12750): The global variables below should not be here as they are AES128 specific. +// ciphertext_length (2) + 14 bytes pkcs#7 AES padding. +pub(crate) global HEADER_CIPHERTEXT_SIZE_IN_BYTES: u32 = 16; + +pub global EPH_PK_X_SIZE_IN_FIELDS: u32 = 1; +pub global EPH_PK_SIGN_BYTE_SIZE_IN_BYTES: u32 = 1; + +// (17 - 1) * 31 - 16 - 1 = 479 +pub global PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES: u32 = ( + PRIVATE_LOG_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS +) + * 31 + - HEADER_CIPHERTEXT_SIZE_IN_BYTES + - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES; +// Each field of the original note log was serialized to 32 bytes. Below we convert the bytes back to fields. +// 479 / 32 = 15 +pub global PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS: u32 = PRIVATE_LOG_PLAINTEXT_SIZE_IN_BYTES / 32; + +/// Trait for encrypting and decrypting private logs in the Aztec protocol. +/// +/// This trait defines the interface for encrypting plaintext data into private logs that can be +/// emitted on-chain or delivered out-of-band, and decrypting those logs back into their original plaintext. +/// +/// # Type Parameters +/// - `PLAINTEXT_LEN`: Length of the plaintext array in fields +/// - `PRIVATE_LOG_CIPHERTEXT_LEN`: Fixed length of encrypted logs (defined globally) +/// - `PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS`: Maximum size of decrypted plaintext (defined globally) +/// +/// # Note on privacy sets +/// To preserve privacy, `encrypt_log` returns a fixed-length array ensuring all log types are indistinguishable +/// on-chain. Implementations of this trait must handle padding the encrypted log to match this standardized length. +pub trait LogEncryption { + /// Encrypts a plaintext field array into a private log that can be emitted on-chain. + /// + /// # Arguments + /// * `plaintext` - Array of Field elements to encrypt + /// * `recipient` - Aztec address of intended recipient who can decrypt the log + /// + /// # Returns + /// Fixed-size array of encrypted Field elements representing the private log + fn encrypt_log( + plaintext: [Field; PLAINTEXT_LEN], + recipient: AztecAddress, + ) -> [Field; PRIVATE_LOG_CIPHERTEXT_LEN]; + + /// Decrypts a private log back into its original plaintext fields. + /// This function is unconstrained since decryption happens when processing logs in an unconstrained context. + /// + /// # Arguments + /// * `ciphertext` - Bounded vector containing the encrypted log fields + /// * `recipient` - Aztec address of the recipient who can decrypt + /// + /// # Returns + /// Bounded vector containing the decrypted plaintext fields + /// + /// # Note on use of BoundedVec + /// `BoundedVec` is required since the log length cannot be determined at compile time. This is because + /// the `Contract::process_log` function is designed to work with all the private logs, emitted by a given contract + /// and not just by 1 log type. + unconstrained fn decrypt_log( + ciphertext: BoundedVec, + recipient: AztecAddress, + ) -> BoundedVec; +} diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/mod.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/mod.nr index 4c39724bd3e9..c84b9b14f6a2 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/mod.nr @@ -1,2 +1,3 @@ pub mod encrypt; pub mod log_assembly_strategies; +pub mod log_encryption; diff --git a/noir-projects/aztec-nr/uint-note/src/uint_note.nr b/noir-projects/aztec-nr/uint-note/src/uint_note.nr index e0889c1167af..b0b761e20a47 100644 --- a/noir-projects/aztec-nr/uint-note/src/uint_note.nr +++ b/noir-projects/aztec-nr/uint-note/src/uint_note.nr @@ -147,13 +147,8 @@ impl UintNote { // id that the recipient will realize that these are the private fields of a partial note. Ideally we'd not rely // on this crude mechanism and we'd instead compute it as a proper event log. However, given the current state // of the log library it's far easier to do it this way. - let encrypted_log = default_aes128::note::compute_log( - *context, - private_log_content, - storage_slot, - recipient, - sender, - ); + let encrypted_log = + default_aes128::note::compute_log(private_log_content, storage_slot, recipient, sender); context.emit_private_log(encrypted_log); PartialUintNote { commitment } diff --git a/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr b/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr index 386b80ba2ab5..44cbe8d5a881 100644 --- a/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr +++ b/noir-projects/noir-contracts/contracts/nft_contract/src/types/nft_note.nr @@ -148,13 +148,8 @@ impl NFTNote { // id that the recipient will realize that these are the private fields of a partial note. Ideally we'd not rely // on this crude mechanism and we'd instead compute it as a proper event log. However, given the current state // of the log library it's far easier to do it this way. - let encrypted_log = default_aes128::note::compute_log( - *context, - private_log_content, - storage_slot, - recipient, - sender, - ); + let encrypted_log = + default_aes128::note::compute_log(private_log_content, storage_slot, recipient, sender); context.emit_private_log(encrypted_log); PartialNFTNote { commitment } diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index 5269ae47072f..16909e78734b 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -283,6 +283,7 @@ pub contract Token { to, from, )); + // We don't constrain encryption of the note log in `transfer` (unlike in `transfer_in_private`) because the transfer // function is only designed to be used in situations where the event is not strictly necessary (e.g. payment to // another person where the payment is considered to be successful when the other party successfully decrypts a