Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions noir-projects/aztec-nr/aztec/src/discovery/mod.nr
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -63,7 +62,7 @@ type ComputeNoteHashAndNullifier<Env> = 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.
Expand Down
32 changes: 20 additions & 12 deletions noir-projects/aztec-nr/aztec/src/discovery/private_logs.nr
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -49,7 +54,10 @@ pub unconstrained fn do_process_log<Env>(
) {
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
Expand Down Expand Up @@ -90,7 +98,7 @@ pub unconstrained fn do_process_log<Env>(
/// - 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:
Expand Down
Original file line number Diff line number Diff line change
@@ -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<let PLAINTEXT_LEN: u32>(
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::<PLAINTEXT_LEN * 32>();
// 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::<PLAINTEXT_LEN * 32>();

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<Field, PRIVATE_LOG_CIPHERTEXT_LEN>,
recipient: AztecAddress,
) -> BoundedVec<Field, PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS> {
let eph_pk_x = ciphertext.get(0);

let ciphertext_without_eph_pk_x_fields = array::subbvec::<Field, PRIVATE_LOG_CIPHERTEXT_LEN, PRIVATE_LOG_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS>(
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::<u8, HEADER_CIPHERTEXT_SIZE_IN_BYTES>::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<u8, (PRIVATE_LOG_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS) * 31 - HEADER_CIPHERTEXT_SIZE_IN_BYTES - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES> =
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::<Field, PRIVATE_LOG_PLAINTEXT_SIZE_IN_FIELDS>::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 {};
}
}
Loading