diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs.nr index 25f085412e5e..61cb92154d4a 100644 --- a/noir-projects/aztec-nr/aztec/src/encrypted_logs.nr +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs.nr @@ -1,2 +1,3 @@ mod header; mod incoming_body; +mod outgoing_body; diff --git a/noir-projects/aztec-nr/aztec/src/encrypted_logs/outgoing_body.nr b/noir-projects/aztec-nr/aztec/src/encrypted_logs/outgoing_body.nr new file mode 100644 index 000000000000..5cd8c736b7db --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/encrypted_logs/outgoing_body.nr @@ -0,0 +1,115 @@ +use dep::protocol_types::{ + address::AztecAddress, grumpkin_private_key::GrumpkinPrivateKey, grumpkin_point::GrumpkinPoint, + constants::GENERATOR_INDEX__SYMMETRIC_KEY +}; + +use dep::std::aes128::aes128_encrypt; +use dep::std::println; + +use crate::keys::point_to_symmetric_key::point_to_symmetric_key; +use crate::hash::poseidon2_hash; + +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 + }; + + use crate::{ + note::{note_header::NoteHeader, note_interface::NoteInterface, utils::compute_note_hash_for_consumption}, + oracle::{unsafe_rand::unsafe_rand, nullifier_key::get_app_nullifier_secret_key, get_public_key::get_public_key}, + context::PrivateContext, hash::poseidon2_hash + }; + + #[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()); + } +} diff --git a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr index 16f8cfe19c99..2c4c24a43c36 100644 --- a/noir-projects/noir-contracts/contracts/test_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test_contract/src/main.nr @@ -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; @@ -374,6 +375,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, diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index e89316b4230e..ed54657b9509 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -125,6 +125,7 @@ export { SiblingPath, EncryptedLogHeader, EncryptedLogIncomingBody, + EncryptedLogOutgoingBody, } from '@aztec/circuit-types'; export { NodeInfo } from '@aztec/types/interfaces'; diff --git a/yarn-project/circuit-types/src/logs/encrypted_log_outgoing_body.test.ts b/yarn-project/circuit-types/src/logs/encrypted_log_outgoing_body.test.ts new file mode 100644 index 000000000000..5f7a35079e51 --- /dev/null +++ b/yarn-project/circuit-types/src/logs/encrypted_log_outgoing_body.test.ts @@ -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, + ); + }); +}); diff --git a/yarn-project/circuit-types/src/logs/encrypted_log_outgoing_body.ts b/yarn-project/circuit-types/src/logs/encrypted_log_outgoing_body.ts new file mode 100644 index 000000000000..e3fb98a7404a --- /dev/null +++ b/yarn-project/circuit-types/src/logs/encrypted_log_outgoing_body.ts @@ -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(); + } +} diff --git a/yarn-project/circuit-types/src/logs/index.ts b/yarn-project/circuit-types/src/logs/index.ts index 2b91857d0956..0e4b8200391b 100644 --- a/yarn-project/circuit-types/src/logs/index.ts +++ b/yarn-project/circuit-types/src/logs/index.ts @@ -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'; diff --git a/yarn-project/end-to-end/src/e2e_encryption.test.ts b/yarn-project/end-to-end/src/e2e_encryption.test.ts index 88abfabd6946..a87653fdc89a 100644 --- a/yarn-project/end-to-end/src/e2e_encryption.test.ts +++ b/yarn-project/end-to-end/src/e2e_encryption.test.ts @@ -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'; @@ -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()); + }); });