Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add encrypted log outgoing body #6334

Merged
merged 2 commits into from
May 17, 2024
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
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());
});
});
Loading