Skip to content
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] Removed `SingleKeyAccountContract`

The `SchnorrSingleKeyAccount` contract and its TypeScript wrapper `SingleKeyAccountContract` have been removed. This contract was insecure: it used `ivpk_m` (incoming viewing public key) as its Schnorr signing key, meaning anyone who received a user's viewing key could sign transactions on their behalf.
Expand Down
86 changes: 51 additions & 35 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 @@ -308,11 +312,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 @@ -349,31 +349,24 @@ 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.
// Pack message bytes into fields
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;

let mut offset = 1;
// Mask each field with a Poseidon2-derived value, so that they appear as uniformly random `Field` values
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[1 + i] = message_bytes_as_fields[i] + mask;
}
offset += message_bytes_as_fields.len();

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.

// Pad with random fields so that padding is indistinguishable from masked data fields.
for i in (1 + message_bytes_as_fields.len())..MESSAGE_CIPHERTEXT_LEN {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why let mut offset was useful 🫠

// 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() };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See how scary it seems that here we do ciphertext[i] but above we did [i + 1]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't that scared to be honest 😅

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

``
for i in 0..len
ciphertext[offset + i]


😭 

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was literally like that before my PR 😅

}

ciphertext
Expand All @@ -385,21 +378,21 @@ 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 fields = masked_fields.mapi(|i, field| unmask_field(ciphertext_shared_secret, i, field));
let ciphertext_without_eph_pk_x = bytes_from_fields(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 @@ -440,6 +433,29 @@ 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 zero if it doesn't (random padding).
fn unmask_field(shared_secret: Point, index: u32, masked: Field) -> Field {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make this unconstrained (so that we dont accidentally call it in constranined fns) and have it return Option<Field>, and then explicitly above do unmask_field(...).unwrap_or(0), instead of hiding how we handle this here. Handling the failure to unmask above by explaining that those are fields that we'll probably ignore anyway is easier to follow/audit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, that is much better

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

embrace monads

let unmasked = masked - derive_field_mask(shared_secret, index);
if unmasked.lt(TWO_POW_248) {
unmasked
} else {
0
}
}

/// Derives a field mask from an ECDH shared secret and field index. Applied only to data fields (those carrying
/// packed message bytes) so the sender is incentivized to mask correctly. 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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are adding the index to the separator instead of the inputs to be consistent with extract_many_close_to_uniformly_random_256_bits_from_ecdh_shared_secret_using_poseidon2_unsafe (another function in this file that does something similar)

)
}

/// 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;
Comment thread
nventuro marked this conversation as resolved.

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