Skip to content

Commit

Permalink
feat: add encrypted log outgoing body (AztecProtocol#6334)
Browse files Browse the repository at this point in the history
  • Loading branch information
LHerskind authored May 17, 2024
1 parent 90d8092 commit fa9f442
Show file tree
Hide file tree
Showing 8 changed files with 320 additions and 1 deletion.
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
@@ -1,2 +1,3 @@
mod header;
mod incoming_body;
mod outgoing_body;
110 changes: 110 additions & 0 deletions noir-projects/aztec-nr/aztec/src/encrypted_logs/outgoing_body.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use dep::protocol_types::{
address::AztecAddress, grumpkin_private_key::GrumpkinPrivateKey, grumpkin_point::GrumpkinPoint,
constants::GENERATOR_INDEX__SYMMETRIC_KEY, hash::poseidon2_hash
};

use dep::std::aes128::aes128_encrypt;
use dep::std::println;

use crate::keys::point_to_symmetric_key::point_to_symmetric_key;

struct EncryptedLogOutgoingBody {
eph_sk: GrumpkinPrivateKey,
recipient: AztecAddress,
recipient_ivpk_app: GrumpkinPoint,
}

impl EncryptedLogOutgoingBody {
pub fn new(
eph_sk: GrumpkinPrivateKey,
recipient: AztecAddress,
recipient_ivpk_app: GrumpkinPoint
) -> Self {
Self { eph_sk, recipient, recipient_ivpk_app }
}

pub fn compute_ciphertext(self, ovsk_app: GrumpkinPrivateKey, eph_pk: GrumpkinPoint) -> [u8; 176] {
// Again, we could compute `eph_pk` here, but we keep the interface more similar
// and also make it easier to optimise it later as we just pass it along

let mut buffer: [u8; 160] = [0; 160];

let serialized_eph_sk: [Field; 2] = self.eph_sk.serialize();
let serialized_eph_sk_high = serialized_eph_sk[0].to_be_bytes(32);
let serialized_eph_sk_low = serialized_eph_sk[1].to_be_bytes(32);

let address_bytes = self.recipient.to_field().to_be_bytes(32);
let serialized_recipient_ivpk_app = self.recipient_ivpk_app.serialize();
let serialized_recipient_ivpk_app_x = serialized_recipient_ivpk_app[0].to_be_bytes(32);
let serialized_recipient_ivpk_app_y = serialized_recipient_ivpk_app[1].to_be_bytes(32);

for i in 0..32 {
buffer[i] = serialized_eph_sk_high[i];
buffer[i + 32] = serialized_eph_sk_low[i];
buffer[i + 64] = address_bytes[i];
buffer[i + 96] = serialized_recipient_ivpk_app_x[i];
buffer[i + 128] = serialized_recipient_ivpk_app_y[i];
}

// We compute the symmetric key using poseidon.
let full_key: [u8; 32] = poseidon2_hash(
[
ovsk_app.high, ovsk_app.low, eph_pk.x, eph_pk.y,
GENERATOR_INDEX__SYMMETRIC_KEY as Field
]
).to_be_bytes(32).as_array();

let mut sym_key = [0; 16];
let mut iv = [0; 16];

for i in 0..16 {
sym_key[i] = full_key[i];
iv[i] = full_key[i + 16];
}
aes128_encrypt(buffer, iv, sym_key).as_array()
}
}

mod test {
use crate::encrypted_logs::outgoing_body::EncryptedLogOutgoingBody;
use dep::protocol_types::{
address::AztecAddress, traits::Empty, constants::GENERATOR_INDEX__NOTE_NULLIFIER,
grumpkin_private_key::GrumpkinPrivateKey, grumpkin_point::GrumpkinPoint, hash::poseidon2_hash
};

use crate::context::PrivateContext;

#[test]
fn test_encrypted_log_outgoing_body() {
let eph_sk = GrumpkinPrivateKey::new(
0x000000000000000000000000000000000f096b423017226a18461115fa8d34bb,
0x00000000000000000000000000000000d0d302ee245dfaf2807e604eec4715fe
);
let recipient_ivsk_app = GrumpkinPrivateKey::new(
0x000000000000000000000000000000000f4d97c25d578f9348251a71ca17ae31,
0x000000000000000000000000000000004828f8f95676ebb481df163f87fd4022
);
let sender_ovsk_app = GrumpkinPrivateKey::new(
0x00000000000000000000000000000000089c6887cb1446d86c64e81afc78048b,
0x0000000000000000000000000000000074d2e28c6bc5176ac02cf7c7d36a444e
);

let eph_pk = eph_sk.derive_public_key();
let recipient_ivpk_app = recipient_ivsk_app.derive_public_key();

let recipient = AztecAddress::from_field(0xdeadbeef);

let body = EncryptedLogOutgoingBody::new(eph_sk, recipient, recipient_ivpk_app);

let ciphertext = body.compute_ciphertext(sender_ovsk_app, eph_pk);

let expected_outgoing_body_ciphertext = [
126, 10, 214, 39, 130, 143, 96, 143, 79, 143, 22, 36, 55, 41, 234, 255, 226, 26, 138, 236, 91, 188, 204, 216, 172, 133, 134, 69, 161, 237, 134, 5, 75, 192, 10, 6, 229, 54, 194, 56, 103, 243, 57, 248, 147, 237, 4, 3, 39, 28, 226, 30, 237, 228, 212, 115, 246, 244, 105, 39, 129, 119, 126, 207, 176, 14, 75, 134, 241, 23, 2, 187, 239, 86, 47, 56, 239, 20, 92, 176, 70, 12, 219, 226, 150, 70, 192, 43, 125, 53, 230, 153, 135, 228, 210, 197, 76, 123, 185, 190, 61, 172, 29, 168, 241, 191, 205, 71, 136, 72, 52, 115, 232, 246, 87, 42, 50, 150, 134, 108, 225, 90, 191, 191, 182, 150, 124, 147, 78, 249, 144, 111, 122, 187, 187, 5, 249, 167, 186, 14, 228, 128, 158, 138, 55, 99, 228, 46, 219, 187, 248, 122, 70, 31, 39, 209, 127, 23, 244, 84, 14, 93, 86, 208, 155, 151, 238, 70, 63, 3, 137, 59, 206, 230, 4, 20
];

for i in 0..expected_outgoing_body_ciphertext.len() {
assert_eq(ciphertext[i], expected_outgoing_body_ciphertext[i]);
}
assert_eq(expected_outgoing_body_ciphertext.len(), ciphertext.len());
}
}
12 changes: 12 additions & 0 deletions noir-projects/noir-contracts/contracts/test_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ contract Test {

use dep::aztec::encrypted_logs::header::EncryptedLogHeader;
use dep::aztec::encrypted_logs::incoming_body::EncryptedLogIncomingBody;
use dep::aztec::encrypted_logs::outgoing_body::EncryptedLogOutgoingBody;

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

Expand Down Expand Up @@ -377,6 +378,17 @@ contract Test {
EncryptedLogIncomingBody::new(storage_slot, TestNote::get_note_type_id(), note).compute_ciphertext(secret, point).as_array()
}

#[aztec(private)]
fn compute_outgoing_log_body_ciphertext(
eph_sk: GrumpkinPrivateKey,
recipient: AztecAddress,
recipient_ivpk_app: GrumpkinPoint,
ovsk_app: GrumpkinPrivateKey
) -> [u8; 176] {
let eph_pk = eph_sk.derive_public_key();
EncryptedLogOutgoingBody::new(eph_sk, recipient, recipient_ivpk_app).compute_ciphertext(ovsk_app, eph_pk)
}

#[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 @@ -125,6 +125,7 @@ export {
SiblingPath,
EncryptedLogHeader,
EncryptedLogIncomingBody,
EncryptedLogOutgoingBody,
} from '@aztec/circuit-types';
export { NodeInfo } from '@aztec/types/interfaces';

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { AztecAddress, GrumpkinScalar } from '@aztec/circuits.js';
import { Grumpkin } from '@aztec/circuits.js/barretenberg';
import { updateInlineTestData } from '@aztec/foundation/testing';

import { EncryptedLogOutgoingBody } from './encrypted_log_outgoing_body.js';

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

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

it('encrypt and decrypt a log outgoing body', () => {
const ephSk = GrumpkinScalar.random();
const recipientIvskApp = GrumpkinScalar.random();
const senderOvskApp = GrumpkinScalar.random();

const ephPk = grumpkin.mul(Grumpkin.generator, ephSk);
const recipientIvpkApp = grumpkin.mul(Grumpkin.generator, recipientIvskApp);

const recipientAddress = AztecAddress.random();

const body = new EncryptedLogOutgoingBody(ephSk, recipientAddress, recipientIvpkApp);

const encrypted = body.computeCiphertext(senderOvskApp, ephPk);

const recreated = EncryptedLogOutgoingBody.fromCiphertext(encrypted, senderOvskApp, ephPk);

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

it('encrypt a log outgoing body, generate input for noir test', () => {
const ephSk = new GrumpkinScalar(0x0f096b423017226a18461115fa8d34bbd0d302ee245dfaf2807e604eec4715fen);
const recipientIvskApp = new GrumpkinScalar(0x0f4d97c25d578f9348251a71ca17ae314828f8f95676ebb481df163f87fd4022n);
const senderOvskApp = new GrumpkinScalar(0x089c6887cb1446d86c64e81afc78048b74d2e28c6bc5176ac02cf7c7d36a444en);

const ephPk = grumpkin.mul(Grumpkin.generator, ephSk);
const recipientIvpkApp = grumpkin.mul(Grumpkin.generator, recipientIvskApp);

const recipientAddress = AztecAddress.fromBigInt(BigInt('0xdeadbeef'));

const body = new EncryptedLogOutgoingBody(ephSk, recipientAddress, recipientIvpkApp);

const encrypted = body.computeCiphertext(senderOvskApp, ephPk);

const recreated = EncryptedLogOutgoingBody.fromCiphertext(encrypted, senderOvskApp, ephPk);

expect(recreated.toBuffer()).toEqual(body.toBuffer());

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/outgoing_body.nr',
'expected_outgoing_body_ciphertext',
byteArrayString,
);
});
});
99 changes: 99 additions & 0 deletions yarn-project/circuit-types/src/logs/encrypted_log_outgoing_body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { AztecAddress, Fr, GeneratorIndex, GrumpkinPrivateKey, Point, type PublicKey } from '@aztec/circuits.js';
import { Aes128 } from '@aztec/circuits.js/barretenberg';
import { poseidon2Hash } from '@aztec/foundation/crypto';
import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize';

export class EncryptedLogOutgoingBody {
constructor(public ephSk: GrumpkinPrivateKey, public recipient: AztecAddress, public recipientIvpkApp: PublicKey) {}

/**
* Serializes the log body
*
* @returns The serialized log body
*/
public toBuffer(): Buffer {
// The serialization of Fq is [high, low] check `grumpkin_private_key.nr`
const ephSkBytes = serializeToBuffer([this.ephSk.high, this.ephSk.low]);
return serializeToBuffer(ephSkBytes, this.recipient, this.recipientIvpkApp);
}

/**
* Deserialized the log body from a buffer
*
* @param buf - The buffer to deserialize
* @returns The deserialized log body
*/
public static fromBuffer(buf: Buffer): EncryptedLogOutgoingBody {
const reader = BufferReader.asReader(buf);
const high = reader.readObject(Fr);
const low = reader.readObject(Fr);
const ephSk = GrumpkinPrivateKey.fromHighLow(high, low);
const recipient = reader.readObject(AztecAddress);
const recipientIvpkApp = reader.readObject(Point); // PublicKey = Point

return new EncryptedLogOutgoingBody(ephSk, recipient, recipientIvpkApp);
}

/**
* Encrypts a log body
*
* @param ovskApp - The app siloed outgoing viewing secret key
* @param ephPk - The ephemeral public key
*
* @returns The ciphertext of the encrypted log body
*/
public computeCiphertext(ovskApp: GrumpkinPrivateKey, ephPk: PublicKey) {
// We could use `ephSk` and compute `ephPk` from it.
// We mainly provide it to keep the same api and potentially slight optimization as we can reuse it.

const aesSecret = EncryptedLogOutgoingBody.derivePoseidonAESSecret(ovskApp, ephPk);

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

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

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

/**
* Decrypts a log body
*
* @param ciphertext - The ciphertext buffer
* @param ovskApp - The app siloed outgoing viewing secret key
* @param ephPk - The ephemeral public key
*
* @returns The decrypted log body
*/
public static fromCiphertext(
ciphertext: Buffer | bigint[],
ovskApp: GrumpkinPrivateKey,
ephPk: PublicKey,
): EncryptedLogOutgoingBody {
const input = Buffer.isBuffer(ciphertext) ? ciphertext : Buffer.from(ciphertext.map((x: bigint) => Number(x)));

const aesSecret = EncryptedLogOutgoingBody.derivePoseidonAESSecret(ovskApp, ephPk);
const key = aesSecret.subarray(0, 16);
const iv = aesSecret.subarray(16, 32);

const aes128 = new Aes128();
const buffer = aes128.decryptBufferCBC(input, iv, key);

return EncryptedLogOutgoingBody.fromBuffer(buffer);
}

/**
* Derives an AES symmetric key from the app siloed outgoing viewing secret key
* and the ephemeral public key using poseidon.
*
* @param ovskApp - The app siloed outgoing viewing secret key
* @param ephPk - The ephemeral public key
* @returns
*/
static derivePoseidonAESSecret(ovskApp: GrumpkinPrivateKey, ephPk: PublicKey) {
// For performance reasons, we do NOT use the usual `deriveAESSecret` function here
// Instead we compute the using using poseidon
return poseidon2Hash([ovskApp.high, ovskApp.low, ephPk.x, ephPk.y, GeneratorIndex.SYMMETRIC_KEY]).toBuffer();
}
}
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 @@ -12,3 +12,4 @@ export * from './unencrypted_l2_log.js';
export * from './extended_unencrypted_l2_log.js';
export * from './encrypted_log_header.js';
export * from './encrypted_log_incoming_body.js';
export * from './encrypted_log_outgoing_body.js';
34 changes: 33 additions & 1 deletion yarn-project/end-to-end/src/e2e_encryption.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { EncryptedLogHeader, EncryptedLogIncomingBody, Fr, GrumpkinScalar, Note, type Wallet } from '@aztec/aztec.js';
import {
AztecAddress,
EncryptedLogHeader,
EncryptedLogIncomingBody,
EncryptedLogOutgoingBody,
Fr,
GrumpkinScalar,
Note,
type Wallet,
} from '@aztec/aztec.js';
import { Aes128, Grumpkin } from '@aztec/circuits.js/barretenberg';
import { TestContract } from '@aztec/noir-contracts.js';

Expand Down Expand Up @@ -99,4 +108,27 @@ describe('e2e_encryption', () => {

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

it('encrypts log outgoing body', async () => {
const ephSk = GrumpkinScalar.random();
const recipientIvskApp = GrumpkinScalar.random();
const senderOvskApp = GrumpkinScalar.random();

const ephPk = grumpkin.mul(Grumpkin.generator, ephSk);
const recipientIvpkApp = grumpkin.mul(Grumpkin.generator, recipientIvskApp);

const recipientAddress = AztecAddress.fromBigInt(BigInt('0xdeadbeef'));

const body = new EncryptedLogOutgoingBody(ephSk, recipientAddress, recipientIvpkApp);

const encrypted = await contract.methods
.compute_outgoing_log_body_ciphertext(ephSk, recipientAddress, recipientIvpkApp, senderOvskApp)
.simulate();

expect(Buffer.from(encrypted.map((x: bigint) => Number(x)))).toEqual(body.computeCiphertext(senderOvskApp, ephPk));

const recreated = EncryptedLogOutgoingBody.fromCiphertext(encrypted, senderOvskApp, ephPk);

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

0 comments on commit fa9f442

Please sign in to comment.