Skip to content
Closed
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
56 changes: 55 additions & 1 deletion noir-projects/aztec-nr/aztec/src/keys/ephemeral.nr
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ use std::embedded_curve_ops::{EmbeddedCurveScalar, fixed_base_scalar_mul};

use crate::protocol::{point::Point, scalar::Scalar};

use crate::oracle::random::random;
use crate::{oracle::random::random, utils::point::get_sign_of_point};

/// Generates a random ephemeral key pair.
pub fn generate_ephemeral_key_pair() -> (Scalar, Point) {
// @todo Need to draw randomness from the full domain of Fq not only Fr

Expand All @@ -20,3 +21,56 @@ pub fn generate_ephemeral_key_pair() -> (Scalar, Point) {

(eph_sk, eph_pk)
}

/// Generates a random ephemeral key pair with a positive y-coordinate.
///
/// Unlike [`generate_ephemeral_key_pair`], the y-coordinate of the public key is guaranteed to be a positive value
/// (i.e. [`crate::utils::point::get_sign_of_point`] will return `true`).
///
/// This is useful as it means it is possible to just broadcast the x-coordinate as a single `Field` and then
/// reconstruct the original public key using [`crate::utils::point::point_from_x_coord_and_sign`] with `sign: true`.
pub fn generate_positive_ephemeral_key_pair() -> (Scalar, Point) {
// Safety: we use the randomness to preserve the privacy of both the sender and recipient via encryption, so a
// malicious sender could use non-random values to reveal the plaintext. But they already know it themselves
// anyway, and so the recipient already trusts them to not disclose this information. We can therefore assume that
// the sender will cooperate in the random value generation.
let eph_sk = unsafe { generate_secret_key_for_positive_public_key() };
let eph_pk = fixed_base_scalar_mul(eph_sk);

assert(get_sign_of_point(eph_pk), "Got an ephemeral public key with a negative y coordinate");

(eph_sk, eph_pk)
}

unconstrained fn generate_secret_key_for_positive_public_key() -> EmbeddedCurveScalar {
let mut sk = std::mem::zeroed();

loop {
// We simply produce random secret keys until we find one that has results in a positive public key. About half
// of all public keys fulfill this condition, so this should only take a few iterations at most.

// @todo Need to draw randomness from the full domain of Fq not only Fr
sk = EmbeddedCurveScalar::from_field(random());
let pk = fixed_base_scalar_mul(sk);
if get_sign_of_point(pk) {
break;
}
}

sk
}

mod test {
use crate::utils::point::get_sign_of_point;
use super::generate_positive_ephemeral_key_pair;

#[test]
fn generate_positive_ephemeral_key_pair_produces_positive_keys() {
// About half of random points are negative, so testing just a couple gives us high confidence that
// `generate_positive_ephemeral_key_pair` is indeed producing positive ones.
for _ in 0..10 {
let (_, pk) = generate_positive_ephemeral_key_pair();
assert(get_sign_of_point(pk));
}
}
}
8 changes: 3 additions & 5 deletions noir-projects/aztec-nr/aztec/src/messages/encoding.nr
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,13 @@ pub global MESSAGE_CIPHERTEXT_LEN: u32 = PRIVATE_LOG_CIPHERTEXT_LEN;
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 Note: We multiply by 31 because ciphertext bytes are stored in fields using
// (17 - 1) * 31 - 16 = 480. Note: We multiply by 31 because ciphertext bytes are stored in fields using
// bytes_to_fields, which packs 31 bytes per field (since a Field is ~254 bits and can safely store 31 whole bytes).
global MESSAGE_PLAINTEXT_SIZE_IN_BYTES: u32 = (MESSAGE_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS) * 31
- HEADER_CIPHERTEXT_SIZE_IN_BYTES
- EPH_PK_SIGN_BYTE_SIZE_IN_BYTES;
- HEADER_CIPHERTEXT_SIZE_IN_BYTES;
// The plaintext bytes represent Field values that were originally serialized using fields_to_bytes, which converts
// each Field to 32 bytes. To convert the plaintext bytes back to fields, we divide by 32. 479 / 32 = 14
// each Field to 32 bytes. To convert the plaintext bytes back to fields, we divide by 32. 480 / 32 = 15
pub global MESSAGE_PLAINTEXT_LEN: u32 = MESSAGE_PLAINTEXT_SIZE_IN_BYTES / 32;

pub global MESSAGE_EXPANDED_METADATA_LEN: u32 = 1;
Expand Down
123 changes: 97 additions & 26 deletions noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ use crate::protocol::{
};

use crate::{
keys::{ecdh_shared_secret::derive_ecdh_shared_secret, ephemeral::generate_ephemeral_key_pair},
keys::{ecdh_shared_secret::derive_ecdh_shared_secret, ephemeral::generate_positive_ephemeral_key_pair},
messages::{
encoding::{
EPH_PK_SIGN_BYTE_SIZE_IN_BYTES, EPH_PK_X_SIZE_IN_FIELDS, HEADER_CIPHERTEXT_SIZE_IN_BYTES,
MESSAGE_CIPHERTEXT_LEN, MESSAGE_PLAINTEXT_LEN,
EPH_PK_X_SIZE_IN_FIELDS, HEADER_CIPHERTEXT_SIZE_IN_BYTES, MESSAGE_CIPHERTEXT_LEN, MESSAGE_PLAINTEXT_LEN,
},
encryption::message_encryption::MessageEncryption,
logs::arithmetic_generics_utils::{
Expand All @@ -25,7 +24,7 @@ use crate::{
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},
point::point_from_x_coord_and_sign,
random::get_random_bytes,
},
};
Expand Down Expand Up @@ -150,6 +149,87 @@ pub fn derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_poseidon2_u
pub struct AES128 {}

impl MessageEncryption for AES128 {

/// AES128-CBC encryption for Aztec protocol messages.
///
/// ## Overview
///
/// The plaintext is an array of up to `MESSAGE_PLAINTEXT_LEN` fields. The output is always exactly
/// `MESSAGE_CIPHERTEXT_LEN` fields, regardless of plaintext size. Unused trailing fields are filled with
/// random data so that all encrypted messages are indistinguishable by size.
///
/// ## PKCS#7 Padding
///
/// AES operates on 16-byte blocks, so the plaintext must be padded to a multiple of 16. PKCS#7 padding always
/// adds at least 1 byte (so the receiver can always detect and strip it), which means:
/// - 1 B plaintext -> 15 B padding -> 16 B total
/// - 15 B plaintext -> 1 B padding -> 16 B total
/// - 16 B plaintext -> 16 B padding -> 32 B total (full extra block)
///
/// In general: if the plaintext is already a multiple of 16, a full 16-byte padding block is appended.
///
/// ## Encryption Steps
///
/// **1. Body encryption.** The plaintext fields are serialized to bytes (32 bytes per field) and AES-128-CBC
/// encrypted. Since 32 is a multiple of 16, PKCS#7 always adds a full 16-byte padding block (see above):
///
/// ```text
/// +---------------------------------------------+
/// | body ct |
/// | PlaintextLen*32 + 16 B |
/// +-------------------------------+--------------+
/// | encrypted plaintext fields | PKCS#7 (16B) |
/// | (serialized at 32 B each) | |
/// +-------------------------------+--------------+
/// ```
///
/// **2. Header encryption.** The byte length of `body_ct` is stored as a 2-byte big-endian integer. This 2-byte
/// header plaintext is then AES-encrypted; PKCS#7 pads the remaining 14 bytes to fill one 16-byte AES block,
/// producing a 16-byte header ciphertext:
///
/// ```text
/// +---------------------------+
/// | header ct |
/// | 16 B |
/// +--------+------------------+
/// | body ct| PKCS#7 (14B) |
/// | length | |
/// | (2 B) | |
/// +--------+------------------+
/// ```
///
/// ## Wire Format
///
/// 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
/// multiple of 31 so it divides evenly into fields:
///
/// ```text
/// +------------+-------------------------+---------+
/// | header ct | body ct | byte pad|
/// | 16 B | PlaintextLen*32 + 16 B | (random)|
/// +------------+-------------------------+---------+
/// |<-------- 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
/// `MESSAGE_CIPHERTEXT_LEN` total) are filled with random data so that all messages are the same size:
///
/// ```text
/// +----------+-------------------------+-------------------+
/// | eph_pk.x | message-byte fields | random field pad |
/// | | (packed 31 B per field) | |
/// +----------+-------------------------+-------------------+
/// |<------------ MESSAGE_CIPHERTEXT_LEN fields ----------->|
/// ```
///
/// ## Key Derivation
///
/// Two (key, IV) pairs are derived from the ECDH shared secret via Poseidon2 hashing with different domain
/// separators: one pair for the body ciphertext and one for the header ciphertext.
fn encrypt<let PlaintextLen: u32>(
plaintext: [Field; PlaintextLen],
recipient: AztecAddress,
Expand All @@ -158,12 +238,8 @@ impl MessageEncryption for AES128 {
// reversed when processing the message in `process_message_ciphertext`)
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;
// Derive ECDH shared secret with recipient using a fresh ephemeral keypair.
let (eph_sk, eph_pk) = generate_positive_ephemeral_key_pair();

// (not to be confused with the tagging shared secret) TODO (#17158): Currently we unwrap the Option returned
// by derive_ecdh_shared_secret. We need to handle the case where the ephemeral public key is invalid to
Expand Down Expand Up @@ -239,10 +315,8 @@ impl MessageEncryption for AES128 {
"unexpected ciphertext header length",
);

// ***************************************************************************** Prepend / append more bytes of
// data to the ciphertext, before converting back to fields.
// *****************************************************************************

// 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
Expand All @@ -256,8 +330,7 @@ impl MessageEncryption for AES128 {
"Unexpected error: message_bytes.len() should be divisible by 31, by construction.",
);

message_bytes[0] = eph_pk_sign_byte;
let mut offset = 1;
let mut offset = 0;
for i in 0..header_ciphertext_bytes.len() {
message_bytes[offset + i] = header_ciphertext_bytes[i];
}
Expand All @@ -279,7 +352,7 @@ impl MessageEncryption for AES128 {
// computation used to obtain the offset computes the expected value (which we _can_ do in a static check), and
// then add a cheap runtime check to also validate that the offset matches this.
std::static_assert(
1 + header_ciphertext_bytes.len() + ciphertext_bytes.len() + message_bytes_padding_to_mult_31.len()
header_ciphertext_bytes.len() + ciphertext_bytes.len() + message_bytes_padding_to_mult_31.len()
== message_bytes.len(),
"unexpected message length",
);
Expand Down Expand Up @@ -334,12 +407,10 @@ impl MessageEncryption for AES128 {
// 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) != 0;

// With the sign and the x-coordinate of the ephemeral public key, we can reconstruct the point. 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, eph_pk_sign_bool).map(|eph_pk| {
// 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);

Expand All @@ -351,7 +422,7 @@ impl MessageEncryption for AES128 {
let (header_sym_key, header_iv) = pairs[1];

// Extract the header ciphertext
let header_start = EPH_PK_SIGN_BYTE_SIZE_IN_BYTES; // Skip eph_pk_sign byte
let header_start = 0;
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
Expand All @@ -368,9 +439,9 @@ impl MessageEncryption for AES128 {

// Extract and decrypt main ciphertext
let ciphertext_start = header_start + HEADER_CIPHERTEXT_SIZE_IN_BYTES;
let ciphertext_with_padding: [u8; (MESSAGE_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS) * 31 - HEADER_CIPHERTEXT_SIZE_IN_BYTES - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES] =
let ciphertext_with_padding: [u8; (MESSAGE_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS) * 31 - HEADER_CIPHERTEXT_SIZE_IN_BYTES] =
array::subarray(ciphertext_without_eph_pk_x.storage(), ciphertext_start);
let ciphertext: BoundedVec<u8, (MESSAGE_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS) * 31 - HEADER_CIPHERTEXT_SIZE_IN_BYTES - EPH_PK_SIGN_BYTE_SIZE_IN_BYTES> =
let ciphertext: BoundedVec<u8, (MESSAGE_CIPHERTEXT_LEN - EPH_PK_X_SIZE_IN_FIELDS) * 31 - HEADER_CIPHERTEXT_SIZE_IN_BYTES> =
BoundedVec::from_parts(ciphertext_with_padding, ciphertext_length);

// Decrypt main ciphertext and return it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,16 @@ fn get_arr_of_size__ciphertext<let FullPt: u32, let PtAesPadding: u32>(
[0; FullPt + PtAesPadding]
}

// Ok, so we have the following bytes: eph_pk_sign, header_ciphertext, ciphertext: Let mbwop = 1 +
// Ok, so we have the following bytes: header_ciphertext, ciphertext: Let mbwop =
// HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| // aka message bytes without padding
fn get_arr_of_size__message_bytes_without_padding<let Ct: u32>(
_ct: [u8; Ct],
) -> [u8; 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + Ct] {
[0; 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + Ct]
) -> [u8; HEADER_CIPHERTEXT_SIZE_IN_BYTES + Ct] {
[0; HEADER_CIPHERTEXT_SIZE_IN_BYTES + Ct]
}

// Recall:
// mbwop := 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| // aka message bytes without padding
// mbwop := HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| // aka message 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(mbwop
// / 31) - mbwop
// = 31 * ((mbwop + 30) // 31) - mbwop
Expand All @@ -51,16 +51,16 @@ fn get_arr_of_size__message_bytes_padding<let Mbwop: u32>(
[0; (31 * ((Mbwop + 30) / 31)) - Mbwop]
}

// |message_bytes| = 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| + p // aka message bytes (with
// |message_bytes| = HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| + p // aka message bytes (with
// padding) Recall:
// mbwop := 1 + HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| p is the padding
// mbwop := HEADER_CIPHERTEXT_SIZE_IN_BYTES + |ct| p is the padding
fn get_arr_of_size__message_bytes<let MBWOP: u32, let P: u32>(_mbwop: [u8; MBWOP], _p: [u8; P]) -> [u8; MBWOP + P] {
[0; MBWOP + 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__message_bytes_padding__from_PT<let Pt: u32>() -> [u8; ((((((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1) + 30) / 31) * 31) - ((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES + 1))] {
pub(crate) fn get_arr_of_size__message_bytes_padding__from_PT<let Pt: u32>() -> [u8; ((((((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES) + 30) / 31) * 31) - ((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES))] {
let full_pt = get_arr_of_size__full_plaintext::<Pt>();
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);
Expand All @@ -71,7 +71,7 @@ pub(crate) fn get_arr_of_size__message_bytes_padding__from_PT<let Pt: u32>() ->

// 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__message_bytes__from_PT<let Pt: u32>() -> [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)))] {
pub(crate) fn get_arr_of_size__message_bytes__from_PT<let Pt: u32>() -> [u8; (((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES) + ((((((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES) + 30) / 31) * 31) - ((Pt + (16 - (Pt % 16))) + HEADER_CIPHERTEXT_SIZE_IN_BYTES)))] {
let full_pt = get_arr_of_size__full_plaintext::<Pt>();
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);
Expand Down
Loading