Skip to content

Commit

Permalink
feat: aes decryption oracle (#11907)
Browse files Browse the repository at this point in the history
Introduces an AES128 decryption oracle to Aztec.nr. The intention is to
enable logs to be processed (including decryption) in a noir contract.
  • Loading branch information
iAmMichaelConnor authored Feb 13, 2025
1 parent 47fe362 commit c4ce913
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 9 deletions.
4 changes: 3 additions & 1 deletion barretenberg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,9 @@ Code is formatted using `clang-format` and the `./cpp/format.sh` script which is
### Testing

Each module has its own tests. e.g. To build and run `ecc` tests:
Each module has its own tests. See `./cpp/scripts/bb-tests.sh` for an exhaustive list of test module names.

e.g. To build and run `ecc` tests:

```bash
# Replace the `default` preset with whichever preset you want to use
Expand Down
122 changes: 122 additions & 0 deletions noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/// Decrypts a ciphertext, using AES128.
///
/// Returns a padded plaintext, of the same size as the input ciphertext.
/// Note that between 1-16 bytes at the end of the returned plaintext will be pkcs#7 padding.
/// It's up to the calling function to identify and remove that padding.
/// See the tests below for an example of how.
/// It's up to the calling function to determine whether decryption succeeded or failed.
/// See the tests below for an example of how.
unconstrained fn aes128_decrypt_oracle_wrapper<let N: u32>(
ciphertext: [u8; N],
iv: [u8; 16],
sym_key: [u8; 16],
) -> [u8; N] {
aes128_decrypt_oracle(ciphertext, iv, sym_key)
}

#[oracle(aes128Decrypt)]
unconstrained fn aes128_decrypt_oracle<let N: u32>(
ciphertext: [u8; N],
iv: [u8; 16],
sym_key: [u8; 16],
) -> [u8; N] {}

mod test {
use crate::{
encrypted_logs::encrypt::aes128::derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256,
utils::point::point_from_x_coord,
};
use super::aes128_decrypt_oracle_wrapper;
use std::aes128::aes128_encrypt;

#[test]
unconstrained fn aes_encrypt_then_decrypt() {
let ciphertext_shared_secret = point_from_x_coord(1);

let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256(
ciphertext_shared_secret,
);

let plaintext: [u8; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

let ciphertext = aes128_encrypt(plaintext, iv, sym_key);

let received_plaintext = aes128_decrypt_oracle_wrapper(ciphertext, iv, sym_key);
let padding_length = received_plaintext[received_plaintext.len() - 1] as u32;

// A BoundedVec could also be used.
let mut received_plaintext_with_padding_removed = std::collections::vec::Vec::new();
for i in 0..received_plaintext.len() - padding_length {
received_plaintext_with_padding_removed.push(received_plaintext[i]);
}

assert_eq(received_plaintext_with_padding_removed.slice, plaintext.as_slice());
}

global TEST_PLAINTEXT_LENGTH: u32 = 10;
global TEST_MAC_LENGTH: u32 = 32;

#[test(should_fail_with = "mac does not match")]
unconstrained fn aes_encrypt_then_decrypt_with_bad_sym_key_is_caught() {
// The AES decryption oracle will not fail for any ciphertext; it will always
// return some data. As for whether the decryption was successful, it's up
// to the app to check this in a custom way.
// E.g. if it's a note that's been encrypted, then upon decryption, the app
// can check to see if the note hash exists onchain. If it doesn't exist
// onchain, then that's a strong indicator that decryption has failed.
// E.g. for non-note messages, the plaintext could include a MAC. We
// demonstrate what this could look like in this test.
//
// We compute a MAC and we include that MAC in the plaintext. We then encrypt
// this plaintext to get a ciphertext. We broadcast the [ciphertext, mac]
// tuple. The eventual decryptor will expect the mac in the decrypted plaintext
// to match the mac that was broadcast. If not, the recipient knows that
// decryption has failed.
let ciphertext_shared_secret = point_from_x_coord(1);

let (sym_key, iv) = derive_aes_symmetric_key_and_iv_from_ecdh_shared_secret_using_sha256(
ciphertext_shared_secret,
);

let mac_preimage = 0x42;
let mac = std::hash::poseidon2::Poseidon2::hash([mac_preimage], 1);
let mac_as_bytes = mac.to_be_bytes::<TEST_MAC_LENGTH>();

let plaintext: [u8; TEST_PLAINTEXT_LENGTH] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];

// We append the mac to the plaintext. It doesn't necessarily have to be 32 bytes;
// that's quite an extreme length. 16 bytes or 8 bytes might be sufficient, and would
// save on data broadcasting costs.
let mut plaintext_with_mac = [0 as u8; TEST_PLAINTEXT_LENGTH + TEST_MAC_LENGTH];
for i in 0..TEST_PLAINTEXT_LENGTH {
plaintext_with_mac[i] = plaintext[i];
}
for i in 0..TEST_MAC_LENGTH {
plaintext_with_mac[TEST_PLAINTEXT_LENGTH + i] = mac_as_bytes[i];
}

let ciphertext = aes128_encrypt(plaintext_with_mac, iv, sym_key);

// We now would broadcast the tuple [ciphertext, mac] to the network.
// The recipient will then decrypt the ciphertext, and if the mac inside the
// received plaintext matches the mac that was broadcast, then the recipient
// knows that decryption was successful.

// For this test, we intentionally mutate the sym_key to a bad one, so that
// decryption fails. This allows us to explore how the recipient can detect
// failed decryption by checking the decrypted mac against the broadcasted
// mac.
let mut bad_sym_key = sym_key;
bad_sym_key[0] = 0;

let received_plaintext = aes128_decrypt_oracle_wrapper(ciphertext, iv, bad_sym_key);

let mut extracted_mac_as_bytes = [0 as u8; TEST_MAC_LENGTH];
for i in 0..TEST_MAC_LENGTH {
extracted_mac_as_bytes[i] = received_plaintext[TEST_PLAINTEXT_LENGTH + i];
}

// We expect this assertion to fail, because we used a bad sym key.
assert_eq(mac_as_bytes, extracted_mac_as_bytes, "mac does not match");
}
}
13 changes: 7 additions & 6 deletions noir-projects/aztec-nr/aztec/src/oracle/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,25 @@
/// Oracles module
// docs:end:oracles-module

pub mod aes128_decrypt;
pub mod block_header;
pub mod call_private_function;
pub mod capsules;
pub mod enqueue_public_function_call;
pub mod execution;
pub mod execution_cache;
pub mod get_contract_instance;
pub mod get_l1_to_l2_membership_witness;
pub mod get_nullifier_membership_witness;
pub mod get_public_data_witness;
pub mod get_membership_witness;
pub mod keys;
pub mod key_validation_request;
pub mod logs;
pub mod note_discovery;
pub mod random;
pub mod enqueue_public_function_call;
pub mod block_header;
pub mod notes;
pub mod random;
pub mod storage;
pub mod logs;
pub mod capsules;
pub mod execution_cache;

// debug_log oracle is used by both noir-protocol-circuits and this crate and for this reason we just re-export it
// here from protocol circuits.
Expand Down
15 changes: 14 additions & 1 deletion yarn-project/circuits.js/src/barretenberg/crypto/aes128/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,29 @@ export class Aes128 {

/**
* Decrypt a buffer using AES-128-CBC.
* We keep the padding in the returned buffer.
* @param data - Data to decrypt.
* @param iv - AES initialization vector.
* @param key - Key to decrypt with.
* @returns Decrypted data.
*/
public async decryptBufferCBC(data: Uint8Array, iv: Uint8Array, key: Uint8Array) {
public async decryptBufferCBCKeepPadding(data: Uint8Array, iv: Uint8Array, key: Uint8Array): Promise<Buffer> {
const api = await BarretenbergSync.initSingleton();
const paddedBuffer = Buffer.from(
api.aesDecryptBufferCbc(new RawBuffer(data), new RawBuffer(iv), new RawBuffer(key), data.length),
);
return paddedBuffer;
}

/**
* Decrypt a buffer using AES-128-CBC.
* @param data - Data to decrypt.
* @param iv - AES initialization vector.
* @param key - Key to decrypt with.
* @returns Decrypted data.
*/
public async decryptBufferCBC(data: Uint8Array, iv: Uint8Array, key: Uint8Array) {
const paddedBuffer = await this.decryptBufferCBCKeepPadding(data, iv, key);
const paddingToRemove = paddedBuffer[paddedBuffer.length - 1];
return paddedBuffer.subarray(0, paddedBuffer.length - paddingToRemove);
}
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/foundation/src/fields/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export function fromBuffer<T extends BaseField>(buffer: Buffer | BufferReader, f
}

/**
* Constructs a field from a Buffer, but reduces it first.
* Constructs a field from a Buffer, but reduces it first, modulo the field modulus.
* This requires a conversion to a bigint first so the initial underlying representation will be a bigint.
*/
function fromBufferReduce<T extends BaseField>(buffer: Buffer, f: DerivedField<T>) {
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/simulator/src/acvm/oracle/typed_oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,4 +257,8 @@ export abstract class TypedOracle {
copyCapsule(_contractAddress: AztecAddress, _srcKey: Fr, _dstKey: Fr, _numEntries: number): Promise<void> {
throw new OracleMethodNotAvailableError('copyCapsule');
}

aes128Decrypt(_ciphertext: Buffer, _iv: Buffer, _symKey: Buffer): Promise<Buffer> {
throw new OracleMethodNotAvailableError('aes128Decrypt');
}
}
1 change: 1 addition & 0 deletions yarn-project/simulator/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { AcirSimulator } from './simulator.js';
export { ViewDataOracle } from './view_data_oracle.js';
export { DBOracle, ContractClassNotFoundError, ContractNotFoundError } from './db_oracle.js';
export * from './pick_notes.js';
export { ExecutionNoteCache } from './execution_note_cache.js';
Expand Down
11 changes: 11 additions & 0 deletions yarn-project/simulator/src/client/view_data_oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type IndexedTaggingSecret,
type KeyValidationRequest,
} from '@aztec/circuits.js';
import { Aes128 } from '@aztec/circuits.js/barretenberg';
import { siloNullifier } from '@aztec/circuits.js/hash';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { Fr } from '@aztec/foundation/fields';
Expand Down Expand Up @@ -355,4 +356,14 @@ export class ViewDataOracle extends TypedOracle {
}
return this.db.copyCapsule(this.contractAddress, srcSlot, dstSlot, numEntries);
}

// TODO(#11849): consider replacing this oracle with a pure Noir implementation of aes decryption.
public override aes128Decrypt(ciphertext: Buffer, iv: Buffer, symKey: Buffer): Promise<Buffer> {
// Noir can't predict the amount of padding that gets trimmed,
// but it needs to know the length of the returned value.
// So we tell Noir that the length is the (predictable) length
// of the padded plaintext, we return that padded plaintext, and have Noir interpret the padding to do the trimming.
const aes128 = new Aes128();
return aes128.decryptBufferCBCKeepPadding(ciphertext, iv, symKey);
}
}
15 changes: 15 additions & 0 deletions yarn-project/txe/src/oracle/txe_oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
type NoteData,
Oracle,
type TypedOracle,
ViewDataOracle,
WASMSimulator,
extractCallStack,
extractPrivateCircuitPublicInputs,
Expand Down Expand Up @@ -115,6 +116,7 @@ export class TXE implements TypedOracle {

private contractDataOracle: ContractDataOracle;
private simulatorOracle: SimulatorOracle;
private viewDataOracle: ViewDataOracle;

private publicDataWrites: PublicDataWrite[] = [];
private uniqueNoteHashesFromPublic: Fr[] = [];
Expand Down Expand Up @@ -159,6 +161,15 @@ export class TXE implements TypedOracle {
this.simulationProvider,
);

this.viewDataOracle = new ViewDataOracle(
this.contractAddress,
[] /* authWitnesses */,
this.simulatorOracle, // note: SimulatorOracle implements DBOracle
this.node,
/* log, */
/* scopes, */
);

this.debug = createDebugOnlyLogger('aztec:kv-pxe-database');
}

Expand Down Expand Up @@ -1187,4 +1198,8 @@ export class TXE implements TypedOracle {
}
return this.txeDatabase.copyCapsule(this.contractAddress, srcSlot, dstSlot, numEntries);
}

aes128Decrypt(ciphertext: Buffer, iv: Buffer, symKey: Buffer): Promise<Buffer> {
return this.viewDataOracle.aes128Decrypt(ciphertext, iv, symKey);
}
}
14 changes: 14 additions & 0 deletions yarn-project/txe/src/txe_service/txe_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
addressFromSingle,
fromArray,
fromSingle,
fromUintArray,
toArray,
toForeignCallResult,
toSingle,
Expand Down Expand Up @@ -580,6 +581,19 @@ export class TXEService {
return toForeignCallResult([]);
}

// TODO: I forgot to add a corresponding function here, when I introduced an oracle method to txe_oracle.ts. The compiler didn't throw an error, so it took me a while to learn of the existence of this file, and that I need to implement this function here. Isn't there a way to programmatically identify that this is missing, given the existence of a txe_oracle method?
async aes128Decrypt(ciphertext: ForeignCallArray, iv: ForeignCallArray, symKey: ForeignCallArray) {
const ciphertextBuffer = fromUintArray(ciphertext, 8);
const ivBuffer = fromUintArray(iv, 8);
const symKeyBuffer = fromUintArray(symKey, 8);

const paddedPlaintext = await this.typedOracle.aes128Decrypt(ciphertextBuffer, ivBuffer, symKeyBuffer);

// We convert each byte of the buffer to its own Field, so that the Noir
// function correctly receives [u8; N].
return toForeignCallResult([toArray(Array.from(paddedPlaintext).map(byte => new Fr(byte)))]);
}

// AVM opcodes

avmOpcodeEmitUnencryptedLog(_message: ForeignCallArray) {
Expand Down
13 changes: 13 additions & 0 deletions yarn-project/txe/src/util/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ export function fromArray(obj: ForeignCallArray) {
return obj.map(str => Fr.fromBuffer(hexToBuffer(str)));
}

/**
* Converts an array of Noir unsigned integers to a single tightly-packed buffer.
* @param uintBitSize If it's an array of Noir u8's, put `8`, etc.
* @returns
*/
export function fromUintArray(obj: ForeignCallArray, uintBitSize: number) {
if (uintBitSize % 8 !== 0) {
throw new Error(`u${uintBitSize} is not a supported type in Noir`);
}
const uintByteSize = uintBitSize / 8;
return Buffer.concat(obj.map(str => hexToBuffer(str).slice(-uintByteSize)));
}

export function toSingle(obj: Fr | AztecAddress) {
return obj.toString().slice(2);
}
Expand Down

0 comments on commit c4ce913

Please sign in to comment.