diff --git a/barretenberg/cpp/src/barretenberg/ecc/curves/grumpkin/c_bind.cpp b/barretenberg/cpp/src/barretenberg/ecc/curves/grumpkin/c_bind.cpp index c7fac7ef7c6..78bb4a04fc5 100644 --- a/barretenberg/cpp/src/barretenberg/ecc/curves/grumpkin/c_bind.cpp +++ b/barretenberg/cpp/src/barretenberg/ecc/curves/grumpkin/c_bind.cpp @@ -15,6 +15,17 @@ WASM_EXPORT void ecc_grumpkin__mul(uint8_t const* point_buf, uint8_t const* scal write(result, r); } +// Silencing warnings about reserved identifiers. Fixing would break downstream code that calls our WASM API. +// NOLINTBEGIN(cert-dcl37-c, cert-dcl51-cpp, bugprone-reserved-identifier) +WASM_EXPORT void ecc_grumpkin__add(uint8_t const* point_a_buf, uint8_t const* point_b_buf, uint8_t* result) +{ + using serialize::write; + auto point_a = from_buffer(point_a_buf); + auto point_b = from_buffer(point_b_buf); + grumpkin::g1::affine_element r = point_a + point_b; + write(result, r); +} + // multiplies a vector of points by a single scalar. Returns a vector of points (this is NOT a multi-exponentiation) WASM_EXPORT void ecc_grumpkin__batch_mul(uint8_t const* point_buf, uint8_t const* scalar_buf, diff --git a/yarn-project/circuit-types/src/logs/encrypted_log_payload.test.ts b/yarn-project/circuit-types/src/logs/encrypted_log_payload.test.ts new file mode 100644 index 00000000000..bb62424faf0 --- /dev/null +++ b/yarn-project/circuit-types/src/logs/encrypted_log_payload.test.ts @@ -0,0 +1,42 @@ +import { AztecAddress, GrumpkinScalar } from '@aztec/circuits.js'; +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; + +import { EncryptedLogPayload } from './encrypted_log_payload.js'; +import { L1NotePayload } from './l1_note_payload/l1_note_payload.js'; + +describe('encrypt and decrypt a full log', () => { + let grumpkin: Grumpkin; + + let ovsk: GrumpkinScalar; + let ivsk: GrumpkinScalar; + + let payload: EncryptedLogPayload; + let encrypted: Buffer; + + beforeAll(() => { + grumpkin = new Grumpkin(); + + ovsk = GrumpkinScalar.random(); + ivsk = GrumpkinScalar.random(); + + const ephSk = GrumpkinScalar.random(); + + const recipientAddress = AztecAddress.random(); + const ivpk = grumpkin.mul(Grumpkin.generator, ivsk); + + payload = EncryptedLogPayload.fromL1NotePayload(L1NotePayload.random()); + encrypted = payload.encrypt(ephSk, recipientAddress, ivpk, ovsk); + }); + + it('decrypt a log as incoming', () => { + const recreated = EncryptedLogPayload.decryptAsIncoming(encrypted, ivsk); + + expect(recreated.toBuffer()).toEqual(payload.toBuffer()); + }); + + it('decrypt a log as outgoing', () => { + const recreated = EncryptedLogPayload.decryptAsOutgoing(encrypted, ovsk); + + expect(recreated.toBuffer()).toEqual(payload.toBuffer()); + }); +}); diff --git a/yarn-project/circuit-types/src/logs/encrypted_log_payload.ts b/yarn-project/circuit-types/src/logs/encrypted_log_payload.ts new file mode 100644 index 00000000000..6ef1cc82add --- /dev/null +++ b/yarn-project/circuit-types/src/logs/encrypted_log_payload.ts @@ -0,0 +1,209 @@ +import { + AztecAddress, + Fr, + type GrumpkinPrivateKey, + Point, + type PublicKey, + computeIvpkApp, + computeIvskApp, + computeOvskApp, + derivePublicKeyFromSecretKey, +} from '@aztec/circuits.js'; +import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize'; + +import { EncryptedLogHeader } from './encrypted_log_header.js'; +import { EncryptedLogIncomingBody } from './encrypted_log_incoming_body.js'; +import { EncryptedLogOutgoingBody } from './encrypted_log_outgoing_body.js'; +import { type L1NotePayload } from './l1_note_payload/l1_note_payload.js'; +import { Note } from './l1_note_payload/note.js'; + +// A placeholder tag until we have a proper tag system in place. +const PLACEHOLDER_TAG = new Fr(33); + +// Both the incoming and the outgoing header are 48 bytes. +// 32 bytes for the address, and 16 bytes padding to follow PKCS#7 +const HEADER_SIZE = 48; + +// The outgoing body is constant size of 176 bytes. +// 160 bytes for the secret key, address, and public key, and 16 bytes padding to follow PKCS#7 +const OUTGOING_BODY_SIZE = 176; + +export class EncryptedLogPayload { + constructor( + /** + * A note as emitted from Noir contract. Can be used along with private key to compute nullifier. + */ + public note: Note, + /** + * Address of the contract this tx is interacting with. + */ + public contractAddress: AztecAddress, + /** + * Storage slot of the underlying note. + */ + public storageSlot: Fr, + /** + * Type identifier for the underlying note, required to determine how to compute its hash and nullifier. + */ + public noteTypeId: Fr, + ) {} + + toBuffer() { + return serializeToBuffer([this.note, this.contractAddress, this.storageSlot, this.noteTypeId]); + } + + static fromBuffer(buffer: Buffer | BufferReader): EncryptedLogPayload { + const reader = BufferReader.asReader(buffer); + return new EncryptedLogPayload( + reader.readObject(Note), + reader.readObject(AztecAddress), + Fr.fromBuffer(reader), + Fr.fromBuffer(reader), + ); + } + + static fromL1NotePayload(l1NotePayload: L1NotePayload) { + return new EncryptedLogPayload( + l1NotePayload.note, + l1NotePayload.contractAddress, + l1NotePayload.storageSlot, + l1NotePayload.noteTypeId, + ); + } + + /** + * Encrypts a note payload for a given recipient and sender. + * Creates an incoming log the the recipient using the recipient's ivsk, and + * an outgoing log for the sender using the sender's ovsk. + * + * @param ephSk - An ephemeral secret key used for the encryption + * @param recipient - The recipient address, retrievable by the sender for his logs + * @param ivpk - The incoming viewing public key of the recipient + * @param ovsk - The outgoing viewing secret key of the sender + * @returns A buffer containing the encrypted log payload + */ + public encrypt(ephSk: GrumpkinPrivateKey, recipient: AztecAddress, ivpk: PublicKey, ovsk: GrumpkinPrivateKey) { + const ephPk = derivePublicKeyFromSecretKey(ephSk); + const ovpk = derivePublicKeyFromSecretKey(ovsk); + + const header = new EncryptedLogHeader(this.contractAddress); + + const incomingHeaderCiphertext = header.computeCiphertext(ephSk, ivpk); + const outgoingHeaderCiphertext = header.computeCiphertext(ephSk, ovpk); + + const ivpkApp = computeIvpkApp(ivpk, this.contractAddress); + + const incomingBodyCiphertext = new EncryptedLogIncomingBody( + this.storageSlot, + this.noteTypeId, + this.note, + ).computeCiphertext(ephSk, ivpkApp); + + const ovskApp = computeOvskApp(ovsk, this.contractAddress); + + const outgoingBodyCiphertext = new EncryptedLogOutgoingBody(ephSk, recipient, ivpkApp).computeCiphertext( + ovskApp, + ephPk, + ); + + return Buffer.concat([ + PLACEHOLDER_TAG.toBuffer(), + PLACEHOLDER_TAG.toBuffer(), + ephPk.toBuffer(), + incomingHeaderCiphertext, + outgoingHeaderCiphertext, + outgoingBodyCiphertext, + incomingBodyCiphertext, + ]); + } + + /** + * Decrypts a ciphertext as an incoming log. + * + * This is executable by the recipient of the note, and uses the ivsk to decrypt the payload. + * The outgoing parts of the log are ignored entirely. + * + * Produces the same output as `decryptAsOutgoing`. + * + * @param ciphertext - The ciphertext for the log + * @param ivsk - The incoming viewing secret key, used to decrypt the logs + * @returns The decrypted log payload + */ + public static decryptAsIncoming(ciphertext: Buffer | bigint[], ivsk: GrumpkinPrivateKey) { + const input = Buffer.isBuffer(ciphertext) ? ciphertext : Buffer.from(ciphertext.map((x: bigint) => Number(x))); + const reader = BufferReader.asReader(input); + + // We don't use the tags as part of the decryption here, we just gotta read to skip them. + reader.readObject(Fr); // incoming tag + reader.readObject(Fr); // outgoing tag + + const ephPk = reader.readObject(Point); + + const incomingHeader = EncryptedLogHeader.fromCiphertext(reader.readBytes(HEADER_SIZE), ivsk, ephPk); + + // Skipping the outgoing header and body + reader.readBytes(HEADER_SIZE); + reader.readBytes(OUTGOING_BODY_SIZE); + + // The incoming can be of variable size, so we read until the end + const incomingBodySlice = reader.readToEnd(); + + const ivskApp = computeIvskApp(ivsk, incomingHeader.address); + const incomingBody = EncryptedLogIncomingBody.fromCiphertext(incomingBodySlice, ivskApp, ephPk); + + return new EncryptedLogPayload( + incomingBody.note, + incomingHeader.address, + incomingBody.storageSlot, + incomingBody.noteTypeId, + ); + } + + /** + * Decrypts a ciphertext as an outgoing log. + * + * This is executable by the sender of the note, and uses the ovsk to decrypt the payload. + * The outgoing parts are decrypted to retrieve information that allows the sender to + * decrypt the incoming log, and learn about the note contents. + * + * Produces the same output as `decryptAsIncoming`. + * + * @param ciphertext - The ciphertext for the log + * @param ovsk - The outgoing viewing secret key, used to decrypt the logs + * @returns The decrypted log payload + */ + public static decryptAsOutgoing(ciphertext: Buffer | bigint[], ovsk: GrumpkinPrivateKey) { + const input = Buffer.isBuffer(ciphertext) ? ciphertext : Buffer.from(ciphertext.map((x: bigint) => Number(x))); + const reader = BufferReader.asReader(input); + + // We don't use the tags as part of the decryption here, we just gotta read to skip them. + reader.readObject(Fr); // incoming tag + reader.readObject(Fr); // outgoing tag + + const ephPk = reader.readObject(Point); + + // Skip the incoming header + reader.readBytes(HEADER_SIZE); + + const outgoingHeader = EncryptedLogHeader.fromCiphertext(reader.readBytes(HEADER_SIZE), ovsk, ephPk); + + const ovskApp = computeOvskApp(ovsk, outgoingHeader.address); + const outgoingBody = EncryptedLogOutgoingBody.fromCiphertext(reader.readBytes(OUTGOING_BODY_SIZE), ovskApp, ephPk); + + // The incoming can be of variable size, so we read until the end + const incomingBodySlice = reader.readToEnd(); + + const incomingBody = EncryptedLogIncomingBody.fromCiphertext( + incomingBodySlice, + outgoingBody.ephSk, + outgoingBody.recipientIvpkApp, + ); + + return new EncryptedLogPayload( + incomingBody.note, + outgoingHeader.address, + incomingBody.storageSlot, + incomingBody.noteTypeId, + ); + } +} diff --git a/yarn-project/circuits.js/src/barretenberg/crypto/grumpkin/index.ts b/yarn-project/circuits.js/src/barretenberg/crypto/grumpkin/index.ts index b9ea7e72325..27a6f736bd6 100644 --- a/yarn-project/circuits.js/src/barretenberg/crypto/grumpkin/index.ts +++ b/yarn-project/circuits.js/src/barretenberg/crypto/grumpkin/index.ts @@ -36,6 +36,19 @@ export class Grumpkin { return Point.fromBuffer(Buffer.from(this.wasm.getMemorySlice(96, 160))); } + /** + * Add two points. + * @param a - Point a in the addition + * @param b - Point b to add to a + * @returns Result of the addition. + */ + public add(a: Point, b: Point): Point { + this.wasm.writeMemory(0, a.toBuffer()); + this.wasm.writeMemory(64, b.toBuffer()); + this.wasm.call('ecc_grumpkin__add', 0, 64, 128); + return Point.fromBuffer(Buffer.from(this.wasm.getMemorySlice(128, 192))); + } + /** * Multiplies a set of points by a scalar. * @param points - Points to multiply. diff --git a/yarn-project/circuits.js/src/keys/index.ts b/yarn-project/circuits.js/src/keys/index.ts index a994d96e5ae..2dc73210e08 100644 --- a/yarn-project/circuits.js/src/keys/index.ts +++ b/yarn-project/circuits.js/src/keys/index.ts @@ -1,16 +1,36 @@ import { AztecAddress } from '@aztec/foundation/aztec-address'; import { poseidon2Hash, sha512ToGrumpkinScalar } from '@aztec/foundation/crypto'; -import { type Fq, type Fr, type GrumpkinScalar } from '@aztec/foundation/fields'; +import { Fq, type Fr, type GrumpkinScalar } from '@aztec/foundation/fields'; import { Grumpkin } from '../barretenberg/crypto/grumpkin/index.js'; import { GeneratorIndex } from '../constants.gen.js'; -import { type GrumpkinPrivateKey } from '../types/grumpkin_private_key.js'; +import { GrumpkinPrivateKey } from '../types/grumpkin_private_key.js'; +import { type PublicKey } from '../types/public_key.js'; import { PublicKeys } from '../types/public_keys.js'; +const curve = new Grumpkin(); + export function computeAppNullifierSecretKey(masterNullifierSecretKey: GrumpkinPrivateKey, app: AztecAddress): Fr { return poseidon2Hash([masterNullifierSecretKey.high, masterNullifierSecretKey.low, app, GeneratorIndex.NSK_M]); } +export function computeIvpkApp(ivpk: PublicKey, address: AztecAddress) { + const I = Fq.fromBuffer(poseidon2Hash([address.toField(), ivpk.x, ivpk.y, GeneratorIndex.IVSK_M]).toBuffer()); + return curve.add(curve.mul(Grumpkin.generator, I), ivpk); +} + +export function computeIvskApp(ivsk: GrumpkinPrivateKey, address: AztecAddress) { + const ivpk = curve.mul(Grumpkin.generator, ivsk); + const I = Fq.fromBuffer(poseidon2Hash([address.toField(), ivpk.x, ivpk.y, GeneratorIndex.IVSK_M]).toBuffer()); + return new Fq((I.toBigInt() + ivsk.toBigInt()) % Fq.MODULUS); +} + +export function computeOvskApp(ovsk: GrumpkinPrivateKey, address: AztecAddress) { + return GrumpkinPrivateKey.fromBuffer( + poseidon2Hash([address.toField(), ovsk.high, ovsk.low, GeneratorIndex.OVSK_M]).toBuffer(), + ); +} + export function deriveMasterNullifierSecretKey(secretKey: Fr): GrumpkinScalar { return sha512ToGrumpkinScalar([secretKey, GeneratorIndex.NSK_M]); }