Skip to content
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
11 changes: 11 additions & 0 deletions barretenberg/cpp/src/barretenberg/ecc/curves/grumpkin/c_bind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<grumpkin::g1::affine_element>(point_a_buf);
auto point_b = from_buffer<grumpkin::g1::affine_element>(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,
Expand Down
42 changes: 42 additions & 0 deletions yarn-project/circuit-types/src/logs/encrypted_log_payload.test.ts
Original file line number Diff line number Diff line change
@@ -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());
});
});
209 changes: 209 additions & 0 deletions yarn-project/circuit-types/src/logs/encrypted_log_payload.ts
Original file line number Diff line number Diff line change
@@ -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);
Comment thread
LHerskind marked this conversation as resolved.

// 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) {
Comment thread
LHerskind marked this conversation as resolved.
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,
);
}
}
13 changes: 13 additions & 0 deletions yarn-project/circuits.js/src/barretenberg/crypto/grumpkin/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 22 additions & 2 deletions yarn-project/circuits.js/src/keys/index.ts
Original file line number Diff line number Diff line change
@@ -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]);
}
Expand Down