From c4ce9132e240cd3ac7953f9d3ddbd0317ca86817 Mon Sep 17 00:00:00 2001 From: Michael Connor Date: Thu, 13 Feb 2025 09:59:19 +0000 Subject: [PATCH] feat: aes decryption oracle (#11907) Introduces an AES128 decryption oracle to Aztec.nr. The intention is to enable logs to be processed (including decryption) in a noir contract. --- barretenberg/README.md | 4 +- .../aztec/src/oracle/aes128_decrypt.nr | 122 ++++++++++++++++++ .../aztec-nr/aztec/src/oracle/mod.nr | 13 +- .../src/barretenberg/crypto/aes128/index.ts | 15 ++- yarn-project/foundation/src/fields/fields.ts | 2 +- .../simulator/src/acvm/oracle/typed_oracle.ts | 4 + yarn-project/simulator/src/client/index.ts | 1 + .../simulator/src/client/view_data_oracle.ts | 11 ++ yarn-project/txe/src/oracle/txe_oracle.ts | 15 +++ .../txe/src/txe_service/txe_service.ts | 14 ++ yarn-project/txe/src/util/encoding.ts | 13 ++ 11 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr diff --git a/barretenberg/README.md b/barretenberg/README.md index 998c1cd29e3..2abedaca8b2 100644 --- a/barretenberg/README.md +++ b/barretenberg/README.md @@ -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 diff --git a/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr new file mode 100644 index 00000000000..5ca72abf49c --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/oracle/aes128_decrypt.nr @@ -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( + 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( + 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::(); + + 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"); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr index 7edfefe7d7d..5d2a6941819 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/mod.nr @@ -2,8 +2,13 @@ /// 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; @@ -11,15 +16,11 @@ 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. diff --git a/yarn-project/circuits.js/src/barretenberg/crypto/aes128/index.ts b/yarn-project/circuits.js/src/barretenberg/crypto/aes128/index.ts index 83e17d17c70..7e8ec680517 100644 --- a/yarn-project/circuits.js/src/barretenberg/crypto/aes128/index.ts +++ b/yarn-project/circuits.js/src/barretenberg/crypto/aes128/index.ts @@ -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 { 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); } diff --git a/yarn-project/foundation/src/fields/fields.ts b/yarn-project/foundation/src/fields/fields.ts index 594a9bf981d..42fe65b5019 100644 --- a/yarn-project/foundation/src/fields/fields.ts +++ b/yarn-project/foundation/src/fields/fields.ts @@ -166,7 +166,7 @@ export function fromBuffer(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(buffer: Buffer, f: DerivedField) { diff --git a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts index 7b87099bdce..92e0364624b 100644 --- a/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts +++ b/yarn-project/simulator/src/acvm/oracle/typed_oracle.ts @@ -257,4 +257,8 @@ export abstract class TypedOracle { copyCapsule(_contractAddress: AztecAddress, _srcKey: Fr, _dstKey: Fr, _numEntries: number): Promise { throw new OracleMethodNotAvailableError('copyCapsule'); } + + aes128Decrypt(_ciphertext: Buffer, _iv: Buffer, _symKey: Buffer): Promise { + throw new OracleMethodNotAvailableError('aes128Decrypt'); + } } diff --git a/yarn-project/simulator/src/client/index.ts b/yarn-project/simulator/src/client/index.ts index 608da797f92..53082090a1c 100644 --- a/yarn-project/simulator/src/client/index.ts +++ b/yarn-project/simulator/src/client/index.ts @@ -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'; diff --git a/yarn-project/simulator/src/client/view_data_oracle.ts b/yarn-project/simulator/src/client/view_data_oracle.ts index d43c8c87bfe..5a12a1c05a0 100644 --- a/yarn-project/simulator/src/client/view_data_oracle.ts +++ b/yarn-project/simulator/src/client/view_data_oracle.ts @@ -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'; @@ -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 { + // 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); + } } diff --git a/yarn-project/txe/src/oracle/txe_oracle.ts b/yarn-project/txe/src/oracle/txe_oracle.ts index 9b79fe13f0a..59cbeb0a611 100644 --- a/yarn-project/txe/src/oracle/txe_oracle.ts +++ b/yarn-project/txe/src/oracle/txe_oracle.ts @@ -81,6 +81,7 @@ import { type NoteData, Oracle, type TypedOracle, + ViewDataOracle, WASMSimulator, extractCallStack, extractPrivateCircuitPublicInputs, @@ -115,6 +116,7 @@ export class TXE implements TypedOracle { private contractDataOracle: ContractDataOracle; private simulatorOracle: SimulatorOracle; + private viewDataOracle: ViewDataOracle; private publicDataWrites: PublicDataWrite[] = []; private uniqueNoteHashesFromPublic: Fr[] = []; @@ -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'); } @@ -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 { + return this.viewDataOracle.aes128Decrypt(ciphertext, iv, symKey); + } } diff --git a/yarn-project/txe/src/txe_service/txe_service.ts b/yarn-project/txe/src/txe_service/txe_service.ts index eb66ed5389e..4726bd4cbb4 100644 --- a/yarn-project/txe/src/txe_service/txe_service.ts +++ b/yarn-project/txe/src/txe_service/txe_service.ts @@ -27,6 +27,7 @@ import { addressFromSingle, fromArray, fromSingle, + fromUintArray, toArray, toForeignCallResult, toSingle, @@ -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) { diff --git a/yarn-project/txe/src/util/encoding.ts b/yarn-project/txe/src/util/encoding.ts index 69719069d8a..5200090f2c0 100644 --- a/yarn-project/txe/src/util/encoding.ts +++ b/yarn-project/txe/src/util/encoding.ts @@ -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); }