Skip to content

Commit

Permalink
feat: add ciphertext computation for log header (#6175)
Browse files Browse the repository at this point in the history
Fixes #5867 with the AES oracle on the noir side and a encrypt/decrypt
tool on the typescript side as well.

Changes the symmetric key derivation to use `GrumpkinPrivateKey` instead
of the `GrumpkinScalar`, this changes the order of low/high.
  • Loading branch information
LHerskind authored May 7, 2024
1 parent 9a644ba commit 3e05534
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 12 deletions.
1 change: 1 addition & 0 deletions noir-projects/aztec-nr/aztec/src/encrypted_logs.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod header;
57 changes: 57 additions & 0 deletions noir-projects/aztec-nr/aztec/src/encrypted_logs/header.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use dep::protocol_types::{address::AztecAddress, grumpkin_private_key::GrumpkinPrivateKey, grumpkin_point::GrumpkinPoint};

use crate::oracle::encryption::aes128_encrypt;
use crate::keys::point_to_symmetric_key::point_to_symmetric_key;

struct EncryptedLogHeader {
address: AztecAddress,
}

impl EncryptedLogHeader {
fn new(address: AztecAddress) -> Self {
EncryptedLogHeader { address }
}

// @todo Issue(#5901) Figure out if we return the bytes or fields for the log
fn compute_ciphertext(self, secret: GrumpkinPrivateKey, point: GrumpkinPoint) -> [u8; 32] {
let full_key = point_to_symmetric_key(secret, point);
let mut sym_key = [0; 16];
let mut iv = [0; 16];
let mut input = [0; 32];
let input_slice = self.address.to_field().to_be_bytes(32);

for i in 0..16 {
sym_key[i] = full_key[i];
iv[i] = full_key[i + 16];

// We copy address on the following 2 lines in order to avoid having 2 loops
input[i] = input_slice[i];
input[i + 16] = input_slice[i + 16];
}

// @todo Issue(#6172) This encryption is currently using an oracle. It is not actually constrained atm.
aes128_encrypt(input, iv, sym_key)
}
}

// @todo Issue(#6172) This is to be run as a test. But it is currently using the AES oracle so will fail there.
fn test_encrypted_log_header() {
let address = AztecAddress::from_field(0xdeadbeef);
let header = EncryptedLogHeader::new(address);
let secret = GrumpkinPrivateKey::new(
0x0000000000000000000000000000000023b3127c127b1f29a7adff5cccf8fb06,
0x00000000000000000000000000000000649e7ca01d9de27b21624098b897babd
);
let point = GrumpkinPoint::new(
0x2688431c705a5ff3e6c6f2573c9e3ba1c1026d2251d0dbbf2d810aa53fd1d186,
0x1e96887b117afca01c00468264f4f80b5bb16d94c1808a448595f115556e5c8e
);

let ciphertext = header.compute_ciphertext(secret, point);

let expected_header_ciphertext = [
131, 119, 105, 129, 244, 32, 151, 205, 12, 99, 93, 62, 10, 180, 72, 21, 179, 36, 250, 95, 56, 167, 171, 16, 195, 164, 223, 57, 75, 5, 24, 119
];

assert_eq(ciphertext, expected_header_ciphertext);
}
10 changes: 5 additions & 5 deletions noir-projects/aztec-nr/aztec/src/keys/point_to_symmetric_key.nr
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use dep::protocol_types::{constants::GENERATOR_INDEX__SYMMETRIC_KEY, grumpkin_point::GrumpkinPoint, utils::arr_copy_slice};
use dep::protocol_types::{constants::GENERATOR_INDEX__SYMMETRIC_KEY, grumpkin_private_key::GrumpkinPrivateKey, grumpkin_point::GrumpkinPoint, utils::arr_copy_slice};
use dep::std::{hash::sha256, grumpkin_scalar::GrumpkinScalar, scalar_mul::variable_base_embedded_curve};

// TODO(#5726): This function is called deriveAESSecret in TS. I don't like point_to_symmetric_key name much since
// point is not the only input of the function. Unify naming with TS once we have a better name.
pub fn point_to_symmetric_key(secret: GrumpkinScalar, point: GrumpkinPoint) -> [u8; 32] {
pub fn point_to_symmetric_key(secret: GrumpkinPrivateKey, point: GrumpkinPoint) -> [u8; 32] {
let shared_secret_fields = variable_base_embedded_curve(point.x, point.y, secret.low, secret.high);
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/6061): make the func return Point struct directly
let shared_secret = GrumpkinPoint::new(shared_secret_fields[0], shared_secret_fields[1]);
Expand All @@ -16,9 +16,9 @@ pub fn point_to_symmetric_key(secret: GrumpkinScalar, point: GrumpkinPoint) -> [
#[test]
fn check_point_to_symmetric_key() {
// Value taken from "derive shared secret" test in encrypt_buffer.test.ts
let secret = GrumpkinScalar::new(
0x00000000000000000000000000000000649e7ca01d9de27b21624098b897babd,
0x0000000000000000000000000000000023b3127c127b1f29a7adff5cccf8fb06
let secret = GrumpkinPrivateKey::new(
0x0000000000000000000000000000000023b3127c127b1f29a7adff5cccf8fb06,
0x00000000000000000000000000000000649e7ca01d9de27b21624098b897babd
);
let point = GrumpkinPoint::new(
0x2688431c705a5ff3e6c6f2573c9e3ba1c1026d2251d0dbbf2d810aa53fd1d186,
Expand Down
1 change: 1 addition & 0 deletions noir-projects/aztec-nr/aztec/src/lib.nr
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ mod oracle;
mod state_vars;
mod prelude;
mod public_storage;
mod encrypted_logs;
use dep::protocol_types;
11 changes: 9 additions & 2 deletions noir-projects/noir-contracts/contracts/test_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ contract Test {

use dep::aztec::protocol_types::{
abis::private_circuit_public_inputs::PrivateCircuitPublicInputs,
constants::{MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, CANONICAL_KEY_REGISTRY_ADDRESS}, traits::{Serialize, ToField, FromField},
grumpkin_point::GrumpkinPoint
constants::{MAX_NOTE_HASH_READ_REQUESTS_PER_CALL, CANONICAL_KEY_REGISTRY_ADDRESS},
traits::{Serialize, ToField, FromField}, grumpkin_point::GrumpkinPoint, grumpkin_private_key::GrumpkinPrivateKey
};

use dep::aztec::encrypted_logs::header::EncryptedLogHeader;

use dep::aztec::note::constants::MAX_NOTES_PER_PAGE;

use dep::aztec::state_vars::{shared_mutable::SharedMutablePrivateGetter, map::derive_storage_slot_in_map};
Expand Down Expand Up @@ -342,6 +344,11 @@ contract Test {
aes128_encrypt(input, iv, key)
}

#[aztec(private)]
fn compute_note_header_ciphertext(secret: GrumpkinPrivateKey, point: GrumpkinPoint) -> [u8; 32] {
EncryptedLogHeader::new(context.this_address()).compute_ciphertext(secret, point)
}

#[aztec(public)]
fn assert_public_global_vars(
chain_id: Field,
Expand Down
1 change: 1 addition & 0 deletions yarn-project/aztec.js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ export {
mockTx,
Comparator,
SiblingPath,
EncryptedLogHeader,
} from '@aztec/circuit-types';
export { NodeInfo } from '@aztec/types/interfaces';

Expand Down
59 changes: 59 additions & 0 deletions yarn-project/circuit-types/src/logs/encrypted_log_header.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { AztecAddress, GrumpkinScalar } from '@aztec/circuits.js';
import { Grumpkin } from '@aztec/circuits.js/barretenberg';
import { updateInlineTestData } from '@aztec/foundation/testing';

import { EncryptedLogHeader } from './encrypted_log_header.js';

describe('encrypt log header', () => {
let grumpkin: Grumpkin;

beforeAll(() => {
grumpkin = new Grumpkin();
});

it('encrypt and decrypt a log header', () => {
const ephSecretKey = GrumpkinScalar.random();
const viewingSecretKey = GrumpkinScalar.random();

const ephPubKey = grumpkin.mul(Grumpkin.generator, ephSecretKey);
const viewingPubKey = grumpkin.mul(Grumpkin.generator, viewingSecretKey);

const addr = AztecAddress.random();
const header = new EncryptedLogHeader(addr);

const encrypted = header.computeCiphertext(ephSecretKey, viewingPubKey);

const recreated = EncryptedLogHeader.fromCiphertext(encrypted, viewingSecretKey, ephPubKey);

expect(recreated.toBuffer()).toEqual(addr.toBuffer());
});

it('encrypt a log header, generate input for noir test', () => {
// The following 2 are arbitrary fixed values - fixed in order to test a match with Noir
const viewingSecretKey: GrumpkinScalar = new GrumpkinScalar(
0x23b3127c127b1f29a7adff5cccf8fb06649e7ca01d9de27b21624098b897babdn,
);
const ephSecretKey: GrumpkinScalar = new GrumpkinScalar(
0x1fdd0dd8c99b21af8e00d2d130bdc263b36dadcbea84ac5ec9293a0660deca01n,
);

const viewingPubKey = grumpkin.mul(Grumpkin.generator, viewingSecretKey);

const addr = AztecAddress.fromBigInt(BigInt('0xdeadbeef'));
const header = new EncryptedLogHeader(addr);

const encrypted = header.computeCiphertext(ephSecretKey, viewingPubKey);

const byteArrayString = `[${encrypted
.toString('hex')
.match(/.{1,2}/g)!
.map(byte => parseInt(byte, 16))}]`;

// Run with AZTEC_GENERATE_TEST_DATA=1 to update noir test data
updateInlineTestData(
'noir-projects/aztec-nr/aztec/src/encrypted_logs/header.nr',
'expected_header_ciphertext',
byteArrayString,
);
});
});
72 changes: 72 additions & 0 deletions yarn-project/circuit-types/src/logs/encrypted_log_header.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { AztecAddress, type GrumpkinPrivateKey, type PublicKey } from '@aztec/circuits.js';
import { Aes128 } from '@aztec/circuits.js/barretenberg';

import { deriveAESSecret } from './l1_note_payload/encrypt_buffer.js';

/**
* An encrypted log header, containing the address of the log along with utility
* functions to compute and decrypt its ciphertext.
*
* Using AES-128-CBC for encryption.
* Can be used for both incoming and outgoing logs.
*
*/
export class EncryptedLogHeader {
constructor(public readonly address: AztecAddress) {}

/**
* Serializes the log header to a buffer
*
* @returns The serialized log header
*/
public toBuffer(): Buffer {
return this.address.toBuffer();
}

public static fromBuffer(buf: Buffer): EncryptedLogHeader {
return new EncryptedLogHeader(AztecAddress.fromBuffer(buf));
}

/**
* Computes the ciphertext of the encrypted log header
*
* @param secret - An ephemeral secret key
* @param publicKey - The incoming or outgoing viewing key of the "recipient" of this log
* @returns The ciphertext of the encrypted log header
*/
public computeCiphertext(secret: GrumpkinPrivateKey, publicKey: PublicKey) {
const aesSecret = deriveAESSecret(secret, publicKey);
const key = aesSecret.subarray(0, 16);
const iv = aesSecret.subarray(16, 32);

const aes128 = new Aes128();
const buffer = this.address.toBuffer();

return aes128.encryptBufferCBC(buffer, iv, key);
}

/**
*
* @param ciphertext - The ciphertext buffer
* @param secret - The private key matching the public key used in encryption
* @param publicKey - The public key generated with the ephemeral secret key used in encryption
* e.g., eph_sk * G
* @returns
*/
public static fromCiphertext(
ciphertext: Buffer | bigint[],
secret: GrumpkinPrivateKey,
publicKey: PublicKey,
): EncryptedLogHeader {
const input = Buffer.isBuffer(ciphertext) ? ciphertext : Buffer.from(ciphertext.map((x: bigint) => Number(x)));

const aesSecret = deriveAESSecret(secret, publicKey);
const key = aesSecret.subarray(0, 16);
const iv = aesSecret.subarray(16, 32);

const aes128 = new Aes128();
const buffer = aes128.decryptBufferCBC(input, iv, key);
const address = AztecAddress.fromBuffer(buffer);
return new EncryptedLogHeader(address);
}
}
1 change: 1 addition & 0 deletions yarn-project/circuit-types/src/logs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export * from './l1_note_payload/index.js';
export * from './tx_l2_logs.js';
export * from './unencrypted_l2_log.js';
export * from './extended_unencrypted_l2_log.js';
export * from './encrypted_log_header.js';
2 changes: 1 addition & 1 deletion yarn-project/end-to-end/src/e2e_2_pxes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { jest } from '@jest/globals';

import { expectsNumOfEncryptedLogsInTheLastBlockToBe, setup, setupPXEService } from './fixtures/utils.js';

const TIMEOUT = 90_000;
const TIMEOUT = 120_000;

describe('e2e_2_pxes', () => {
jest.setTimeout(TIMEOUT);
Expand Down
22 changes: 19 additions & 3 deletions yarn-project/end-to-end/src/e2e_encryption.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type Wallet } from '@aztec/aztec.js';
import { Aes128 } from '@aztec/circuits.js/barretenberg';
import { EncryptedLogHeader, GrumpkinScalar, type Wallet } from '@aztec/aztec.js';
import { Aes128, Grumpkin } from '@aztec/circuits.js/barretenberg';
import { TestContract } from '@aztec/noir-contracts.js';

import { randomBytes } from 'crypto';
Expand All @@ -8,6 +8,7 @@ import { setup } from './fixtures/utils.js';

describe('e2e_encryption', () => {
const aes128 = new Aes128();
let grumpkin: Grumpkin;

let wallet: Wallet;
let teardown: () => Promise<void>;
Expand All @@ -17,7 +18,8 @@ describe('e2e_encryption', () => {
beforeAll(async () => {
({ teardown, wallet } = await setup());
contract = await TestContract.deploy(wallet).send().deployed();
});
grumpkin = new Grumpkin();
}, 120_000);

afterAll(() => teardown());

Expand Down Expand Up @@ -52,4 +54,18 @@ describe('e2e_encryption', () => {

expect(ciphertext).toEqual(expectedCiphertext);
});

it('encrypts header', async () => {
const ephSecretKey = GrumpkinScalar.random();
const viewingSecretKey = GrumpkinScalar.random();

const ephPubKey = grumpkin.mul(Grumpkin.generator, ephSecretKey);
const viewingPubKey = grumpkin.mul(Grumpkin.generator, viewingSecretKey);

const encrypted = await contract.methods.compute_note_header_ciphertext(ephSecretKey, viewingPubKey).simulate();

const recreated = EncryptedLogHeader.fromCiphertext(encrypted, viewingSecretKey, ephPubKey);

expect(recreated.address).toEqual(contract.address);
});
});
2 changes: 1 addition & 1 deletion yarn-project/end-to-end/src/e2e_key_registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { jest } from '@jest/globals';

import { publicDeployAccounts, setup } from './fixtures/utils.js';

const TIMEOUT = 100_000;
const TIMEOUT = 120_000;

const SHARED_MUTABLE_DELAY = 5;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const consumeNextBlocks = () => {
const log = createDebugLogger('aztec:server_world_state_synchronizer_test');

describe('server_world_state_synchronizer', () => {
jest.setTimeout(30_000);

let db: AztecKVStore;
let l1ToL2Messages: Fr[];
let inHash: Buffer;
Expand Down

0 comments on commit 3e05534

Please sign in to comment.