Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: computing sym key for incoming ciphertext #6020

Merged
merged 11 commits into from
Apr 30, 2024
Merged
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deriveAESSecret as a name will be a pain with the outgoing as it derives an AES secret, but not from the point. secretAndPointToSymmetricKey incoming 😆

// 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);
LHerskind marked this conversation as resolved.
Show resolved Hide resolved
shared_secret_bytes_with_separator[64] = GENERATOR_INDEX__SYMMETRIC_KEY;
sha256(shared_secret_bytes_with_separator)
}

#[test]
fn check_point_to_symmetric_key() {
benesjan marked this conversation as resolved.
Show resolved Hide resolved
// 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
Loading