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: 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]);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Removing the padding will be done basically always, right? Would it make sense to just have a utility function for it? Or have function calling the oracle and removing the padding and then returning pure plaintext?

But if it's just for demonstration purposes now and it will be considered later then I have no issue with it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, it would be good to eventually have a utility function for this, because you're right: everyone will want to do it. I chose not to here, because I wasn't sure what type to choose (between a slice or a Vec or a BoundedVec), and so I thought I'd leave it to whomever implements the actual decoding & decryption strategy for our notes & events, to choose the type that works best when they try. (That person could end up being me).


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);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm re-using the implementation from the viewDataOracle.

}
}
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