Skip to content

Commit

Permalink
feat: full encryption and decryption of log in ts (AztecProtocol#6348)
Browse files Browse the repository at this point in the history
Fixes AztecProtocol#6291. Creates a full example of encrypting and decryption in
typescript.

Needed to add a point addition from BB into grumpkin in typescript since
this is required for the computation of the `ivpk_app`
  • Loading branch information
LHerskind authored May 17, 2024
1 parent 7a31896 commit 0ac83dc
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 2 deletions.
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);

// 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,
);
}
}
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

0 comments on commit 0ac83dc

Please sign in to comment.