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
4 changes: 4 additions & 0 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [Aztec.nr] Removed `get_random_bytes`

The `get_random_bytes` unconstrained function has been removed from `aztec::utils::random`. If you were using it, you can replace it with direct calls to the `random` oracle from `aztec::oracle::random` and convert to bytes yourself.

### [Aztec.js] `simulate()`, `send()`, and deploy return types changed to always return objects

All SDK interaction methods now return structured objects that include offchain output alongside the primary result. This affects `.simulate()`, `.send()`, deploy `.send()`, and `Wallet.sendTx()`.
Expand Down
87 changes: 56 additions & 31 deletions noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::protocol::{
address::AztecAddress,
constants::{DOM_SEP__SYMMETRIC_KEY, DOM_SEP__SYMMETRIC_KEY_2},
constants::{DOM_SEP__CIPHERTEXT_FIELD_MASK, DOM_SEP__SYMMETRIC_KEY, DOM_SEP__SYMMETRIC_KEY_2},
hash::poseidon2_hash_with_separator,
point::Point,
public_keys::AddressPoint,
Expand All @@ -26,7 +26,6 @@ use crate::{
fields_to_bytes::{fields_from_bytes, fields_to_bytes},
},
point::point_from_x_coord_and_sign,
random::get_random_bytes,
},
};

Expand Down Expand Up @@ -156,8 +155,9 @@ impl MessageEncryption for AES128 {
/// ## Overview
///
/// The plaintext is an array of up to `MESSAGE_PLAINTEXT_LEN` (12) fields. The output is always exactly
/// `MESSAGE_CIPHERTEXT_LEN` (15) fields, regardless of plaintext size. Unused trailing fields are filled with
/// random data so that all encrypted messages are indistinguishable by size.
/// `MESSAGE_CIPHERTEXT_LEN` (15) fields, regardless of plaintext size. All output fields except the
/// ephemeral public key are uniformly random `Field` values to any observer without knowledge of the
/// shared secret, making all encrypted messages indistinguishable by size or content.
///
/// ## PKCS#7 Padding
///
Expand Down Expand Up @@ -204,27 +204,31 @@ impl MessageEncryption for AES128 {
/// Messages are transmitted as fields, not bytes. A field is ~254 bits and can safely store 31 whole bytes, so
/// we need to pack our byte data into 31-byte chunks. This packing drives the wire format.
///
/// **Step 1 -- Assemble bytes.** The ciphertexts are laid out in a byte array, padded with random bytes to a
/// **Step 1 -- Assemble bytes.** The ciphertexts are laid out in a byte array, padded with zero bytes to a
/// multiple of 31 so it divides evenly into fields:
///
/// ```text
/// +------------+-------------------------+---------+
/// | header ct | body ct | byte pad|
/// | 16 B | PlaintextLen*32 + 16 B | (random)|
/// | 16 B | PlaintextLen*32 + 16 B | (zeros) |
/// +------------+-------------------------+---------+
/// |<-------- padded to a multiple of 31 B -------->|
/// ```
///
/// **Step 2 -- Pack into fields.** The byte array is split into 31-byte chunks, each stored in one field. The
/// ephemeral public key x-coordinate is prepended as its own field. Any remaining fields (up to 15 total) are
/// filled with random data so that all messages are the same size:
/// **Step 2 -- Pack and mask.** The byte array is split into 31-byte chunks, each stored in one field. A
/// Poseidon2-derived mask (see `derive_field_mask`) is added to each so that the resulting fields appear as
/// uniformly random `Field` values to any observer without knowledge of the shared secret, hiding the fact
/// that the underlying ciphertext consists of 128-bit AES blocks.
///
/// **Step 3 -- Assemble ciphertext.** The ephemeral public key x-coordinate is prepended and random field padding
/// is appended to fill to 15 fields:
///
/// ```text
/// +----------+-------------------------+-------------------+
/// | eph_pk.x | message-byte fields | random field pad |
/// | | (packed 31 B per field) | (fills to 15) |
/// | eph_pk.x | masked message fields | random field pad |
/// | | (packed 31 B per field) | (fills to 15) |
/// +----------+-------------------------+-------------------+
/// |<---------- MESSAGE_CIPHERTEXT_LEN = 15 fields ------->|
/// |<---------- MESSAGE_CIPHERTEXT_LEN = 15 fields -------->|
/// ```
///
/// ## Key Derivation
Expand Down Expand Up @@ -304,11 +308,7 @@ impl MessageEncryption for AES128 {

// Assemble the message byte array:
// [header_ct (16B)] [body_ct] [padding to mult of 31]
let mut message_bytes_padding_to_mult_31 =
get_arr_of_size__message_bytes_padding__from_PT::<PlaintextLen * 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.
message_bytes_padding_to_mult_31 = unsafe { get_random_bytes() };
let message_bytes_padding_to_mult_31 = get_arr_of_size__message_bytes_padding__from_PT::<PlaintextLen * 32>();

let mut message_bytes = get_arr_of_size__message_bytes__from_PT::<PlaintextLen * 32>();

Expand Down Expand Up @@ -346,30 +346,26 @@ impl MessageEncryption for AES128 {
assert(offset == message_bytes.len(), "unexpected encrypted message length");

// Pack message bytes into fields (31 bytes per field) and prepend eph_pk.x.
// TODO(#12749): As Mike pointed out, we need to make messages 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 message_bytes_as_fields = bytes_to_fields(message_bytes);

let mut ciphertext: [Field; MESSAGE_CIPHERTEXT_LEN] = [0; MESSAGE_CIPHERTEXT_LEN];

ciphertext[0] = eph_pk.x;

// Mask each content field with a Poseidon2-derived value, so that they appear as uniformly random `Field`
// values
let mut offset = 1;
for i in 0..message_bytes_as_fields.len() {
ciphertext[offset + i] = message_bytes_as_fields[i];
let mask = derive_field_mask(ciphertext_shared_secret, i as u32);
ciphertext[offset + i] = message_bytes_as_fields[i] + mask;
}
offset += message_bytes_as_fields.len();

// Pad with random fields so that padding is indistinguishable from masked data fields.
for i in offset..MESSAGE_CIPHERTEXT_LEN {
// We need to get a random value that fits in 31 bytes to not leak information about the size of the
// message (all the "real" message 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 message 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[i] = unsafe { random() };
}

ciphertext
Expand All @@ -381,21 +377,26 @@ impl MessageEncryption for AES128 {
) -> Option<BoundedVec<Field, MESSAGE_PLAINTEXT_LEN>> {
let eph_pk_x = ciphertext.get(0);

let ciphertext_without_eph_pk_x_fields = array::subbvec::<Field, MESSAGE_CIPHERTEXT_LEN, MESSAGE_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS>(
let masked_fields = array::subbvec::<Field, MESSAGE_CIPHERTEXT_LEN, MESSAGE_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);

// With the x-coordinate of the ephemeral public key we can reconstruct the point as we know that the
// y-coordinate must be positive. This may fail however, as not all x-coordinates are on the curve. In that
// case, we simply return `Option::none`.
point_from_x_coord_and_sign(eph_pk_x, true).map(|eph_pk| {
// Derive shared secret
let ciphertext_shared_secret = get_shared_secret(recipient, eph_pk);

let unmasked_fields = masked_fields.mapi(|i, field| {
let unmasked = unmask_field(ciphertext_shared_secret, i, field);
// If we failed to unmask the field, we are dealing with the random padding. We'll ignore it later,
// so we can simply set it to 0
unmasked.unwrap_or(0)
});
let ciphertext_without_eph_pk_x = bytes_from_fields(unmasked_fields);

// Derive symmetric keys:
let pairs = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_unsafe::<2>(
ciphertext_shared_secret,
Expand Down Expand Up @@ -436,6 +437,30 @@ impl MessageEncryption for AES128 {
}
}

/// 2^248: upper bound for values that fit in 31 bytes
global TWO_POW_248: Field = 2.pow_32(248);

/// Removes the Poseidon2-derived mask from a ciphertext field. Returns the unmasked value if it fits in 31 bytes
/// (a content field), or `None` if it doesn't (random padding). Unconstrained to prevent accidental use in
/// constrained context.
unconstrained fn unmask_field(shared_secret: Point, index: u32, masked: Field) -> Option<Field> {
let unmasked = masked - derive_field_mask(shared_secret, index);
if unmasked.lt(TWO_POW_248) {
Option::some(unmasked)
} else {
Option::none()
}
}

/// Derives a field mask from an ECDH shared secret and field index. Applied only to data fields (those carrying
/// packed message bytes). Padding fields use `random()` instead.
fn derive_field_mask(shared_secret: Point, index: u32) -> Field {
poseidon2_hash_with_separator(
[shared_secret.x, shared_secret.y],
DOM_SEP__CIPHERTEXT_FIELD_MASK + index,
)
}

/// Produces a random valid address point, i.e. one that is on the curve. This is equivalent to calling
/// [`AztecAddress::to_address_point`] on a random valid address.
unconstrained fn random_address_point() -> AddressPoint {
Expand Down
1 change: 0 additions & 1 deletion noir-projects/aztec-nr/aztec/src/utils/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ pub mod array;
pub mod comparison;
pub mod conversion;
pub mod point;
pub mod random;
mod with_hash;
pub use with_hash::WithHash;
pub mod remove_constraints;
Expand Down
17 changes: 0 additions & 17 deletions noir-projects/aztec-nr/aztec/src/utils/random.nr

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ pub global DOM_SEP__AUTHWIT_NULLIFIER: u32 = 1239150694;

pub global DOM_SEP__SYMMETRIC_KEY: u32 = 3882206064;
pub global DOM_SEP__SYMMETRIC_KEY_2: u32 = 4129434989;
pub global DOM_SEP__CIPHERTEXT_FIELD_MASK: u32 = 1870492847;

pub global DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT: u32 = 623934423;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,19 @@ use crate::{
CONTRACT_CLASS_REGISTRY_UTILITY_FUNCTION_BROADCASTED_MAGIC_VALUE,
CONTRACT_INSTANCE_PUBLISHED_MAGIC_VALUE, CONTRACT_INSTANCE_UPDATED_MAGIC_VALUE,
DOM_SEP__AUTHWIT_INNER, DOM_SEP__AUTHWIT_NULLIFIER, DOM_SEP__AUTHWIT_OUTER,
DOM_SEP__BLOCK_HEADER_HASH, DOM_SEP__CONTRACT_ADDRESS_V1, DOM_SEP__CONTRACT_CLASS_ID,
DOM_SEP__EVENT_COMMITMENT, DOM_SEP__FUNCTION_ARGS, DOM_SEP__INITIALIZATION_NULLIFIER,
DOM_SEP__INITIALIZER, DOM_SEP__IVSK_M, DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__NHK_M,
DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE, DOM_SEP__NOTE_NULLIFIER, DOM_SEP__OVSK_M,
DOM_SEP__PARTIAL_ADDRESS, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT,
DOM_SEP__PRIVATE_FUNCTION_LEAF, DOM_SEP__PRIVATE_LOG_FIRST_FIELD, DOM_SEP__PRIVATE_TX_HASH,
DOM_SEP__PROTOCOL_CONTRACTS, DOM_SEP__PUBLIC_BYTECODE, DOM_SEP__PUBLIC_CALLDATA,
DOM_SEP__PUBLIC_KEYS_HASH, DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT,
DOM_SEP__PUBLIC_TX_HASH, DOM_SEP__SECRET_HASH, DOM_SEP__SIGNATURE_PAYLOAD,
DOM_SEP__SILOED_NOTE_HASH, DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SYMMETRIC_KEY,
DOM_SEP__SYMMETRIC_KEY_2, DOM_SEP__TSK_M, DOM_SEP__TX_NULLIFIER, DOM_SEP__TX_REQUEST,
DOM_SEP__UNIQUE_NOTE_HASH, NULL_MSG_SENDER_CONTRACT_ADDRESS, SIDE_EFFECT_MASKING_ADDRESS,
TX_START_PREFIX,
DOM_SEP__BLOCK_HEADER_HASH, DOM_SEP__CIPHERTEXT_FIELD_MASK, DOM_SEP__CONTRACT_ADDRESS_V1,
DOM_SEP__CONTRACT_CLASS_ID, DOM_SEP__EVENT_COMMITMENT, DOM_SEP__FUNCTION_ARGS,
DOM_SEP__INITIALIZATION_NULLIFIER, DOM_SEP__INITIALIZER, DOM_SEP__IVSK_M,
DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__NHK_M, DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE,
DOM_SEP__NOTE_NULLIFIER, DOM_SEP__OVSK_M, DOM_SEP__PARTIAL_ADDRESS,
DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, DOM_SEP__PRIVATE_FUNCTION_LEAF,
DOM_SEP__PRIVATE_LOG_FIRST_FIELD, DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS,
DOM_SEP__PUBLIC_BYTECODE, DOM_SEP__PUBLIC_CALLDATA, DOM_SEP__PUBLIC_KEYS_HASH,
DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, DOM_SEP__PUBLIC_TX_HASH,
DOM_SEP__SECRET_HASH, DOM_SEP__SIGNATURE_PAYLOAD, DOM_SEP__SILOED_NOTE_HASH,
DOM_SEP__SILOED_NULLIFIER, DOM_SEP__SYMMETRIC_KEY, DOM_SEP__SYMMETRIC_KEY_2, DOM_SEP__TSK_M,
DOM_SEP__TX_NULLIFIER, DOM_SEP__TX_REQUEST, DOM_SEP__UNIQUE_NOTE_HASH,
NULL_MSG_SENDER_CONTRACT_ADDRESS, SIDE_EFFECT_MASKING_ADDRESS, TX_START_PREFIX,
},
hash::poseidon2_hash_bytes,
traits::{FromField, ToField},
Expand Down Expand Up @@ -129,7 +129,7 @@ impl<let NUM_VALUES: u32, let NUM_U32_VALUES: u32> HashedValueTester<NUM_VALUES,

#[test]
fn hashed_values_match_derived() {
let mut tester = HashedValueTester::<49, 42>::new();
let mut tester = HashedValueTester::<50, 43>::new();

// -----------------
// Domain separators
Expand Down Expand Up @@ -174,6 +174,7 @@ fn hashed_values_match_derived() {
tester.assert_dom_sep_matches_derived(DOM_SEP__AUTHWIT_NULLIFIER, "authwit_nullifier");
tester.assert_dom_sep_matches_derived(DOM_SEP__SYMMETRIC_KEY, "symmetric_key");
tester.assert_dom_sep_matches_derived(DOM_SEP__SYMMETRIC_KEY_2, "symmetric_key_2");
tester.assert_dom_sep_matches_derived(DOM_SEP__CIPHERTEXT_FIELD_MASK, "ciphertext_field_mask");
tester.assert_dom_sep_matches_derived(
DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT,
"partial_note_validity_commitment",
Expand Down
1 change: 1 addition & 0 deletions yarn-project/constants/src/constants.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ export enum DomainSeparator {
AUTHWIT_NULLIFIER = 1239150694,
SYMMETRIC_KEY = 3882206064,
SYMMETRIC_KEY_2 = 4129434989,
CIPHERTEXT_FIELD_MASK = 1870492847,
PARTIAL_NOTE_VALIDITY_COMMITMENT = 623934423,
INITIALIZATION_NULLIFIER = 1653084894,
SECRET_HASH = 4199652938,
Expand Down
Loading