Skip to content

Commit

Permalink
feat: computing sym key for incoming ciphertext (#6020)
Browse files Browse the repository at this point in the history
  • Loading branch information
benesjan committed Apr 30, 2024
1 parent 81142fe commit 1904fa8
Show file tree
Hide file tree
Showing 18 changed files with 145 additions and 99 deletions.
14 changes: 8 additions & 6 deletions l1-contracts/src/core/libraries/ConstantsGen.sol
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,10 @@ library Constants {
uint256 internal constant NULLIFIER_KEY_VALIDATION_REQUEST_CONTEXT_LENGTH = 4;
uint256 internal constant PARTIAL_STATE_REFERENCE_LENGTH = 6;
uint256 internal constant READ_REQUEST_LENGTH = 2;
uint256 internal constant NOTE_HASH_LENGTH = 2;
uint256 internal constant NOTE_HASH_CONTEXT_LENGTH = 3;
uint256 internal constant NULLIFIER_LENGTH = 3;
uint256 internal constant SIDE_EFFECT_LENGTH = 2;
uint256 internal constant SIDE_EFFECT_LINKED_TO_NOTE_HASH_LENGTH = 3;
uint256 internal constant STATE_REFERENCE_LENGTH =
APPEND_ONLY_TREE_SNAPSHOT_LENGTH + PARTIAL_STATE_REFERENCE_LENGTH;
uint256 internal constant TX_CONTEXT_LENGTH = 2 + GAS_SETTINGS_LENGTH;
Expand All @@ -130,9 +132,9 @@ library Constants {
+ MAX_BLOCK_NUMBER_LENGTH + (SIDE_EFFECT_LENGTH * MAX_NOTE_HASH_READ_REQUESTS_PER_CALL)
+ (READ_REQUEST_LENGTH * MAX_NULLIFIER_READ_REQUESTS_PER_CALL)
+ (NULLIFIER_KEY_VALIDATION_REQUEST_LENGTH * MAX_NULLIFIER_KEY_VALIDATION_REQUESTS_PER_CALL)
+ (SIDE_EFFECT_LENGTH * MAX_NEW_NOTE_HASHES_PER_CALL)
+ (SIDE_EFFECT_LINKED_TO_NOTE_HASH_LENGTH * MAX_NEW_NULLIFIERS_PER_CALL)
+ MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL + MAX_PUBLIC_CALL_STACK_LENGTH_PER_CALL
+ (NOTE_HASH_LENGTH * MAX_NEW_NOTE_HASHES_PER_CALL)
+ (NULLIFIER_LENGTH * MAX_NEW_NULLIFIERS_PER_CALL) + MAX_PRIVATE_CALL_STACK_LENGTH_PER_CALL
+ MAX_PUBLIC_CALL_STACK_LENGTH_PER_CALL
+ (L2_TO_L1_MESSAGE_LENGTH * MAX_NEW_L2_TO_L1_MSGS_PER_CALL) + 2
+ (SIDE_EFFECT_LENGTH * MAX_ENCRYPTED_LOGS_PER_CALL)
+ (SIDE_EFFECT_LENGTH * MAX_UNENCRYPTED_LOGS_PER_CALL) + 2 + HEADER_LENGTH + TX_CONTEXT_LENGTH;
Expand All @@ -141,8 +143,8 @@ library Constants {
+ (READ_REQUEST_LENGTH * MAX_NULLIFIER_NON_EXISTENT_READ_REQUESTS_PER_CALL)
+ (CONTRACT_STORAGE_UPDATE_REQUEST_LENGTH * MAX_PUBLIC_DATA_UPDATE_REQUESTS_PER_CALL)
+ (CONTRACT_STORAGE_READ_LENGTH * MAX_PUBLIC_DATA_READS_PER_CALL)
+ MAX_PUBLIC_CALL_STACK_LENGTH_PER_CALL + (SIDE_EFFECT_LENGTH * MAX_NEW_NOTE_HASHES_PER_CALL)
+ (SIDE_EFFECT_LINKED_TO_NOTE_HASH_LENGTH * MAX_NEW_NULLIFIERS_PER_CALL)
+ MAX_PUBLIC_CALL_STACK_LENGTH_PER_CALL + (NOTE_HASH_LENGTH * MAX_NEW_NOTE_HASHES_PER_CALL)
+ (NULLIFIER_LENGTH * MAX_NEW_NULLIFIERS_PER_CALL)
+ (L2_TO_L1_MESSAGE_LENGTH * MAX_NEW_L2_TO_L1_MSGS_PER_CALL) + 2
+ (SIDE_EFFECT_LENGTH * MAX_UNENCRYPTED_LOGS_PER_CALL) + 1 + HEADER_LENGTH
+ GLOBAL_VARIABLES_LENGTH + AZTEC_ADDRESS_LENGTH /* revert_code */ + 1 + 2 * GAS_LENGTH /* transaction_fee */
Expand Down
1 change: 1 addition & 0 deletions noir-projects/aztec-nr/aztec/src/keys.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mod point_to_symmetric_key;
34 changes: 34 additions & 0 deletions noir-projects/aztec-nr/aztec/src/keys/point_to_symmetric_key.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use dep::protocol_types::{constants::GENERATOR_INDEX__SYMMETRIC_KEY, grumpkin_point::GrumpkinPoint, utils::arr_copy_slice};
use dep::std::{hash::sha256, grumpkin_scalar::GrumpkinScalar, scalar_mul::variable_base_embedded_curve};

// TODO(#5726): This function is called deriveAESSecret in TS. I don't like point_to_symmetric_key name much since
// point is not the only input of the function. Unify naming with TS once we have a better name.
pub fn point_to_symmetric_key(secret: GrumpkinScalar, point: GrumpkinPoint) -> [u8; 32] {
let shared_secret_fields = variable_base_embedded_curve(point.x, point.y, secret.low, secret.high);
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/6061): make the func return Point struct directly
let shared_secret = GrumpkinPoint::new(shared_secret_fields[0], shared_secret_fields[1]);
let mut shared_secret_bytes_with_separator = [0 as u8; 65];
shared_secret_bytes_with_separator = arr_copy_slice(shared_secret.to_be_bytes(), shared_secret_bytes_with_separator, 0);
shared_secret_bytes_with_separator[64] = GENERATOR_INDEX__SYMMETRIC_KEY;
sha256(shared_secret_bytes_with_separator)
}

#[test]
fn check_point_to_symmetric_key() {
// Value taken from "derive shared secret" test in encrypt_buffer.test.ts
let secret = GrumpkinScalar::new(
0x00000000000000000000000000000000649e7ca01d9de27b21624098b897babd,
0x0000000000000000000000000000000023b3127c127b1f29a7adff5cccf8fb06
);
let point = GrumpkinPoint::new(
0x2688431c705a5ff3e6c6f2573c9e3ba1c1026d2251d0dbbf2d810aa53fd1d186,
0x1e96887b117afca01c00468264f4f80b5bb16d94c1808a448595f115556e5c8e
);

let key = point_to_symmetric_key(secret, point);
// The following value gets updated when running encrypt_buffer.test.ts with AZTEC_GENERATE_TEST_DATA=1
let expected_key = [
198, 74, 242, 51, 177, 36, 183, 8, 2, 246, 197, 138, 59, 166, 86, 96, 155, 50, 186, 34, 242, 3, 208, 144, 161, 64, 69, 165, 70, 57, 226, 139
];
assert_eq(key, expected_key);
}
1 change: 1 addition & 0 deletions noir-projects/aztec-nr/aztec/src/lib.nr
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod deploy;
mod hash;
mod history;
mod initializer;
mod keys;
mod log;
mod messaging;
mod note;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,4 @@ global GENERATOR_INDEX__PUBLIC_KEYS_HASH = 51;
global GENERATOR_INDEX__NOTE_NULLIFIER = 52;
global GENERATOR_INDEX__INNER_NOTE_HASH = 53;
global GENERATOR_INDEX__NOTE_CONTENT_HASH = 54;
global GENERATOR_INDEX__SYMMETRIC_KEY: u8 = 55;
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,15 @@ impl GrumpkinPoint {
assert(self.x == 0);
assert(self.y == 0);
}

pub fn to_be_bytes(self: Self) -> [u8; 64] {
let mut result = [0 as u8; 64];
let x_bytes = self.x.to_be_bytes(32);
let y_bytes = self.y.to_be_bytes(32);
for i in 0..32 {
result[i] = x_bytes[i];
result[i + 32] = y_bytes[i];
}
result
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GrumpkinScalar } from '@aztec/circuits.js';
import { Fq, GrumpkinScalar } from '@aztec/circuits.js';
import { Grumpkin } from '@aztec/circuits.js/barretenberg';
import { randomBytes } from '@aztec/foundation/crypto';
import { updateInlineTestData } from '@aztec/foundation/testing';

import { decryptBuffer, deriveAESSecret, encryptBuffer } from './encrypt_buffer.js';

Expand All @@ -12,38 +13,51 @@ describe('encrypt buffer', () => {
});

it('derive shared secret', () => {
const ownerPrivKey = GrumpkinScalar.random();
const ownerPubKey = grumpkin.mul(Grumpkin.generator, ownerPrivKey);
const ephPrivKey = GrumpkinScalar.random();
const ephPubKey = grumpkin.mul(Grumpkin.generator, ephPrivKey);
// The following 2 are arbitrary fixed values - fixed in order to test a match with Noir
const ownerSecretKey: GrumpkinScalar = new Fq(0x23b3127c127b1f29a7adff5cccf8fb06649e7ca01d9de27b21624098b897babdn);
const ephSecretKey: GrumpkinScalar = new Fq(0x1fdd0dd8c99b21af8e00d2d130bdc263b36dadcbea84ac5ec9293a0660deca01n);

const secretBySender = deriveAESSecret(ownerPubKey, ephPrivKey, grumpkin);
const secretByReceiver = deriveAESSecret(ephPubKey, ownerPrivKey, grumpkin);
const ownerPubKey = grumpkin.mul(Grumpkin.generator, ownerSecretKey);
const ephPubKey = grumpkin.mul(Grumpkin.generator, ephSecretKey);

const secretBySender = deriveAESSecret(ephSecretKey, ownerPubKey);
const secretByReceiver = deriveAESSecret(ownerSecretKey, ephPubKey);
expect(secretBySender.toString('hex')).toEqual(secretByReceiver.toString('hex'));

const byteArrayString = `[${secretBySender
.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/keys/point_to_symmetric_key.nr',
'expected_key',
byteArrayString,
);
});

it('convert to and from encrypted buffer', () => {
const data = randomBytes(253);
const ownerPrivKey = GrumpkinScalar.random();
const ownerPubKey = grumpkin.mul(Grumpkin.generator, ownerPrivKey);
const ephPrivKey = GrumpkinScalar.random();
const encrypted = encryptBuffer(data, ownerPubKey, ephPrivKey, grumpkin);
const decrypted = decryptBuffer(encrypted, ownerPrivKey, grumpkin);
const ownerSecretKey = GrumpkinScalar.random();
const ownerPubKey = grumpkin.mul(Grumpkin.generator, ownerSecretKey);
const ephSecretKey = GrumpkinScalar.random();
const encrypted = encryptBuffer(data, ephSecretKey, ownerPubKey);
const decrypted = decryptBuffer(encrypted, ownerSecretKey);
expect(decrypted).not.toBeUndefined();
expect(decrypted).toEqual(data);
});

it('decrypting gibberish returns undefined', () => {
const data = randomBytes(253);
const ownerPrivKey = GrumpkinScalar.random();
const ephPrivKey = GrumpkinScalar.random();
const ownerPubKey = grumpkin.mul(Grumpkin.generator, ownerPrivKey);
const encrypted = encryptBuffer(data, ownerPubKey, ephPrivKey, grumpkin);
const ownerSecretKey = GrumpkinScalar.random();
const ephSecretKey = GrumpkinScalar.random();
const ownerPubKey = grumpkin.mul(Grumpkin.generator, ownerSecretKey);
const encrypted = encryptBuffer(data, ephSecretKey, ownerPubKey);

// Introduce gibberish.
const gibberish = Buffer.concat([randomBytes(8), encrypted.subarray(8)]);

const decrypted = decryptBuffer(gibberish, ownerPrivKey, grumpkin);
const decrypted = decryptBuffer(gibberish, ownerSecretKey);
expect(decrypted).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type GrumpkinPrivateKey, type PublicKey } from '@aztec/circuits.js';
import { type Grumpkin } from '@aztec/circuits.js/barretenberg';
import { GeneratorIndex, type GrumpkinPrivateKey, type PublicKey } from '@aztec/circuits.js';
import { Grumpkin } from '@aztec/circuits.js/barretenberg';
import { sha256 } from '@aztec/foundation/crypto';
import { Point } from '@aztec/foundation/fields';
import { numToUInt8 } from '@aztec/foundation/serialize';
Expand All @@ -12,14 +12,16 @@ import { createCipheriv, createDecipheriv } from 'browserify-cipher';
* the shared secret. The shared secret is then hashed using SHA-256 to produce the final
* AES secret key.
*
* @param ecdhPubKey - The ECDH public key represented as a PublicKey object.
* @param ecdhPrivKey - The ECDH private key represented as a Buffer object.
* @param grumpkin - The curve to use for curve operations.
* @returns A Buffer containing the derived AES secret key.
* @param secretKey - The secret key used to derive shared secret.
* @param publicKey - The public key used to derive shared secret.
* @returns A derived AES secret key.
* TODO(#5726): This function is called point_to_symmetric_key in Noir. I don't like that name much since point is not
* the only input of the function. Unify naming once we have a better name.
*/
export function deriveAESSecret(ecdhPubKey: PublicKey, ecdhPrivKey: GrumpkinPrivateKey, curve: Grumpkin): Buffer {
const sharedSecret = curve.mul(ecdhPubKey, ecdhPrivKey);
const secretBuffer = Buffer.concat([sharedSecret.toBuffer(), numToUInt8(1)]);
export function deriveAESSecret(secretKey: GrumpkinPrivateKey, publicKey: PublicKey): Buffer {
const curve = new Grumpkin();
const sharedSecret = curve.mul(publicKey, secretKey);
const secretBuffer = Buffer.concat([sharedSecret.toBuffer(), numToUInt8(GeneratorIndex.SYMMETRIC_KEY)]);
const hash = sha256(secretBuffer);
return hash;
}
Expand All @@ -31,40 +33,37 @@ export function deriveAESSecret(ecdhPubKey: PublicKey, ecdhPrivKey: GrumpkinPriv
* with the provided curve instance for elliptic curve operations.
*
* @param data - The data buffer to be encrypted.
* @param ownerPubKey - The owner's public key as a PublicKey instance.
* @param ephPrivKey - The ephemeral private key as a Buffer instance.
* @param curve - The curve instance used for elliptic curve operations.
* @param ephSecretKey - The ephemeral secret key..
* @param incomingViewingPublicKey - The note owner's incoming viewing public key.
* @returns A Buffer containing the encrypted data and the ephemeral public key.
*/
export function encryptBuffer(
data: Buffer,
ownerPubKey: PublicKey,
ephPrivKey: GrumpkinPrivateKey,
curve: Grumpkin,
ephSecretKey: GrumpkinPrivateKey,
incomingViewingPublicKey: PublicKey,
): Buffer {
const aesSecret = deriveAESSecret(ownerPubKey, ephPrivKey, curve);
const aesSecret = deriveAESSecret(ephSecretKey, incomingViewingPublicKey);
const aesKey = aesSecret.subarray(0, 16);
const iv = aesSecret.subarray(16, 32);
const cipher = createCipheriv('aes-128-cbc', aesKey, iv);
const plaintext = Buffer.concat([iv.subarray(0, 8), data]);
const ephPubKey = curve.mul(curve.generator(), ephPrivKey);
const curve = new Grumpkin();
const ephPubKey = curve.mul(curve.generator(), ephSecretKey);

return Buffer.concat([cipher.update(plaintext), cipher.final(), ephPubKey.toBuffer()]);
}

/**
* Decrypts the given encrypted data buffer using the owner's private key and a Grumpkin curve.
* Extracts the ephemeral public key from the input data, derives the AES secret using
* the owner's private key, and decrypts the plaintext.
* If the decryption is successful, returns the decrypted plaintext, otherwise returns undefined.
*
* Decrypts the given encrypted data buffer using the provided secret key.
* @param data - The encrypted data buffer to be decrypted.
* @param ownerPrivKey - The private key of the owner used for decryption.
* @param curve - The curve object used in the decryption process.
* @param incomingViewingSecretKey - The secret key used for decryption.
* @returns The decrypted plaintext as a Buffer or undefined if decryption fails.
*/
export function decryptBuffer(data: Buffer, ownerPrivKey: GrumpkinPrivateKey, curve: Grumpkin): Buffer | undefined {
export function decryptBuffer(data: Buffer, incomingViewingSecretKey: GrumpkinPrivateKey): Buffer | undefined {
// Extract the ephemeral public key from the end of the data
const ephPubKey = Point.fromBuffer(data.subarray(-64));
const aesSecret = deriveAESSecret(ephPubKey, ownerPrivKey, curve);
// Derive the AES secret key using the secret key and the ephemeral public key
const aesSecret = deriveAESSecret(incomingViewingSecretKey, ephPubKey);
const aesKey = aesSecret.subarray(0, 16);
const iv = aesSecret.subarray(16, 32);
const cipher = createDecipheriv('aes-128-cbc', aesKey, iv);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,18 @@ describe('L1 Note Payload', () => {
const payload = L1NotePayload.random();
const ownerPrivKey = GrumpkinScalar.random();
const ownerPubKey = grumpkin.mul(Grumpkin.generator, ownerPrivKey);
const encrypted = payload.toEncryptedBuffer(ownerPubKey, grumpkin);
const decrypted = L1NotePayload.fromEncryptedBuffer(encrypted, ownerPrivKey, grumpkin);
const encrypted = payload.toEncryptedBuffer(ownerPubKey);
const decrypted = L1NotePayload.fromEncryptedBuffer(encrypted, ownerPrivKey);
expect(decrypted).not.toBeUndefined();
expect(decrypted).toEqual(payload);
});

it('return undefined if unable to decrypt the encrypted buffer', () => {
const payload = L1NotePayload.random();
const ownerPubKey = Point.random();
const encrypted = payload.toEncryptedBuffer(ownerPubKey, grumpkin);
const encrypted = payload.toEncryptedBuffer(ownerPubKey);
const randomPrivKey = GrumpkinScalar.random();
const decrypted = L1NotePayload.fromEncryptedBuffer(encrypted, randomPrivKey, grumpkin);
const decrypted = L1NotePayload.fromEncryptedBuffer(encrypted, randomPrivKey);
expect(decrypted).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { AztecAddress, type GrumpkinPrivateKey, type PublicKey } from '@aztec/circuits.js';
import { type Grumpkin } from '@aztec/circuits.js/barretenberg';
import { Fr, GrumpkinScalar } from '@aztec/foundation/fields';
import { BufferReader, serializeToBuffer } from '@aztec/foundation/serialize';

Expand Down Expand Up @@ -56,28 +55,22 @@ export class L1NotePayload {

/**
* Encrypt the L1NotePayload object using the owner's public key and the ephemeral private key.
* @param ownerPubKey - Public key of the owner of the L1NotePayload object.
* @param curve - The curve instance to use.
* @param incomingViewingPubKey - Public key of the owner of the L1NotePayload object.
* @returns The encrypted L1NotePayload object.
*/
public toEncryptedBuffer(ownerPubKey: PublicKey, curve: Grumpkin): Buffer {
const ephPrivKey: GrumpkinPrivateKey = GrumpkinScalar.random();
return encryptBuffer(this.toBuffer(), ownerPubKey, ephPrivKey, curve);
public toEncryptedBuffer(incomingViewingPubKey: PublicKey): Buffer {
const ephSecretKey: GrumpkinPrivateKey = GrumpkinScalar.random();
return encryptBuffer(this.toBuffer(), ephSecretKey, incomingViewingPubKey);
}

/**
* Decrypts the L1NotePayload object using the owner's private key.
* Decrypts the L1NotePayload object using the owner's incoming viewing secret key.
* @param data - Encrypted L1NotePayload object.
* @param ownerPrivKey - Private key of the owner of the L1NotePayload object.
* @param curve - The curve instance to use.
* @param incomingViewingSecretKey - Incoming viewing secret key of the owner of the L1NotePayload object.
* @returns Instance of L1NotePayload if the decryption was successful, undefined otherwise.
*/
static fromEncryptedBuffer(
data: Buffer,
ownerPrivKey: GrumpkinPrivateKey,
curve: Grumpkin,
): L1NotePayload | undefined {
const buf = decryptBuffer(data, ownerPrivKey, curve);
static fromEncryptedBuffer(data: Buffer, incomingViewingSecretKey: GrumpkinPrivateKey): L1NotePayload | undefined {
const buf = decryptBuffer(data, incomingViewingSecretKey);
if (!buf) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ describe('L1 Note Payload', () => {
const taggedNote = new TaggedNote(payload);
const ownerPrivKey = GrumpkinScalar.random();
const ownerPubKey = grumpkin.mul(Grumpkin.generator, ownerPrivKey);
const encrypted = taggedNote.toEncryptedBuffer(ownerPubKey, grumpkin);
const decrypted = TaggedNote.fromEncryptedBuffer(encrypted, ownerPrivKey, grumpkin);
const encrypted = taggedNote.toEncryptedBuffer(ownerPubKey);
const decrypted = TaggedNote.fromEncryptedBuffer(encrypted, ownerPrivKey);
expect(decrypted).not.toBeUndefined();
expect(decrypted?.notePayload).toEqual(payload);
});
Expand All @@ -33,9 +33,9 @@ describe('L1 Note Payload', () => {
const payload = L1NotePayload.random();
const taggedNote = new TaggedNote(payload);
const ownerPubKey = Point.random();
const encrypted = taggedNote.toEncryptedBuffer(ownerPubKey, grumpkin);
const encrypted = taggedNote.toEncryptedBuffer(ownerPubKey);
const randomPrivKey = GrumpkinScalar.random();
const decrypted = TaggedNote.fromEncryptedBuffer(encrypted, randomPrivKey, grumpkin);
const decrypted = TaggedNote.fromEncryptedBuffer(encrypted, randomPrivKey);
expect(decrypted).toBeUndefined();
});
});
Loading

0 comments on commit 1904fa8

Please sign in to comment.