From 3e44a580a3769d7e65124294500ccedab9cdfce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Fri, 12 Apr 2024 15:39:05 +0200 Subject: [PATCH] feat: new key store (#5653) Fixes #5607 --- .../crates/types/src/constants.nr | 9 +- yarn-project/circuit-types/src/keys/index.ts | 1 + .../circuit-types/src/keys/key_store.ts | 1 + .../circuit-types/src/keys/new_key_store.ts | 52 +++++++ yarn-project/circuits.js/src/constants.gen.ts | 6 + yarn-project/foundation/src/crypto/index.ts | 1 + .../foundation/src/crypto/sha512/index.ts | 16 +++ .../key-store/src/new_test_key_store.test.ts | 42 ++++++ .../key-store/src/new_test_key_store.ts | 130 ++++++++++++++++++ yarn-project/key-store/src/test_key_store.ts | 1 + 10 files changed, 257 insertions(+), 2 deletions(-) create mode 100644 yarn-project/circuit-types/src/keys/new_key_store.ts create mode 100644 yarn-project/foundation/src/crypto/sha512/index.ts create mode 100644 yarn-project/key-store/src/new_test_key_store.test.ts create mode 100644 yarn-project/key-store/src/new_test_key_store.ts diff --git a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr index a25b45b2226..63b35b38f27 100644 --- a/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr +++ b/noir-projects/noir-protocol-circuits/crates/types/src/constants.nr @@ -193,8 +193,6 @@ global NUM_BASE_PARITY_PER_ROOT_PARITY: u64 = 4; * | MID | 8 < n ≤ 16 | 32 < hash_index ≤ 40 | * | HIGH | 16 < n ≤ 48 | 40 < hash_index ≤ 48 | * +-----------+-------------------------------+----------------------+ - * - * Note: When modifying, modify `GeneratorIndexPacker` in packer.hpp accordingly. */ // Indices with size ≤ 8 global GENERATOR_INDEX__NOTE_HASH = 1; @@ -238,3 +236,10 @@ global GENERATOR_INDEX__PUBLIC_CIRCUIT_PUBLIC_INPUTS = 43; global GENERATOR_INDEX__FUNCTION_ARGS = 44; global GENERATOR_INDEX__AUTHWIT_INNER = 45; global GENERATOR_INDEX__AUTHWIT_OUTER = 46; +// Key related generators follow +global GENERATOR_INDEX__NSK_M = 47; +global GENERATOR_INDEX__IVSK_M = 48; +global GENERATOR_INDEX__OVSK_M = 49; +global GENERATOR_INDEX__TSK_M = 50; +global GENERATOR_INDEX__PUBLIC_KEYS_HASH = 51; +global GENERATOR_INDEX__CONTRACT_ADDRESS_V1 = 52; diff --git a/yarn-project/circuit-types/src/keys/index.ts b/yarn-project/circuit-types/src/keys/index.ts index f137b0d567a..be16aca18e4 100644 --- a/yarn-project/circuit-types/src/keys/index.ts +++ b/yarn-project/circuit-types/src/keys/index.ts @@ -1,2 +1,3 @@ export * from './key_pair.js'; export * from './key_store.js'; +export * from './new_key_store.js'; diff --git a/yarn-project/circuit-types/src/keys/key_store.ts b/yarn-project/circuit-types/src/keys/key_store.ts index 4869503def4..3cf2c960d59 100644 --- a/yarn-project/circuit-types/src/keys/key_store.ts +++ b/yarn-project/circuit-types/src/keys/key_store.ts @@ -3,6 +3,7 @@ import { type AztecAddress, type GrumpkinPrivateKey, type PublicKey } from '@azt /** * Represents a secure storage for managing keys. * Provides functionality to create and retrieve accounts, private and public keys, + * TODO(#5627): 💣💣💣 */ export interface KeyStore { /** diff --git a/yarn-project/circuit-types/src/keys/new_key_store.ts b/yarn-project/circuit-types/src/keys/new_key_store.ts new file mode 100644 index 00000000000..6ae9337cc18 --- /dev/null +++ b/yarn-project/circuit-types/src/keys/new_key_store.ts @@ -0,0 +1,52 @@ +import { type AztecAddress, type Fr, type PartialAddress, type PublicKey } from '@aztec/circuits.js'; + +/** + * Represents a secure storage for managing keys. + */ +export interface NewKeyStore { + /** + * Creates a new account from a randomly generated secret key. + * @returns A promise that resolves to the newly created account's AztecAddress. + */ + createAccount(): Promise; + + /** + * Adds an account to the key store from the provided secret key. + * @param sk - The secret key of the account. + * @param partialAddress - The partial address of the account. + * @returns The account's address. + */ + addAccount(sk: Fr, partialAddress: PartialAddress): Promise; + + /** + * Gets the master nullifier public key for a given account. + * @throws If the account does not exist in the key store. + * @param account - The account address for which to retrieve the master nullifier public key. + * @returns The master nullifier public key for the account. + */ + getMasterNullifierPublicKey(account: AztecAddress): Promise; + + /** + * Gets the master incoming viewing public key for a given account. + * @throws If the account does not exist in the key store. + * @param account - The account address for which to retrieve the master incoming viewing public key. + * @returns The master incoming viewing public key for the account. + */ + getMasterIncomingViewingPublicKey(account: AztecAddress): Promise; + + /** + * Retrieves the master outgoing viewing key. + * @throws If the account does not exist in the key store. + * @param account - The account to retrieve the master outgoing viewing key for. + * @returns A Promise that resolves to the master outgoing viewing key. + */ + getMasterOutgoingViewingPublicKey(account: AztecAddress): Promise; + + /** + * Retrieves the master tagging key. + * @throws If the account does not exist in the key store. + * @param account - The account to retrieve the master tagging key for. + * @returns A Promise that resolves to the master tagging key. + */ + getMasterTaggingPublicKey(account: AztecAddress): Promise; +} diff --git a/yarn-project/circuits.js/src/constants.gen.ts b/yarn-project/circuits.js/src/constants.gen.ts index 5a8cb29928c..253c792aab9 100644 --- a/yarn-project/circuits.js/src/constants.gen.ts +++ b/yarn-project/circuits.js/src/constants.gen.ts @@ -152,4 +152,10 @@ export enum GeneratorIndex { FUNCTION_ARGS = 44, AUTHWIT_INNER = 45, AUTHWIT_OUTER = 46, + NSK_M = 47, + IVSK_M = 48, + OVSK_M = 49, + TSK_M = 50, + PUBLIC_KEYS_HASH = 51, + CONTRACT_ADDRESS_V1 = 52, } diff --git a/yarn-project/foundation/src/crypto/index.ts b/yarn-project/foundation/src/crypto/index.ts index 96f4df584ef..96365958e92 100644 --- a/yarn-project/foundation/src/crypto/index.ts +++ b/yarn-project/foundation/src/crypto/index.ts @@ -3,6 +3,7 @@ import { BarretenbergSync } from '@aztec/bb.js'; export * from './keccak/index.js'; export * from './random/index.js'; export * from './sha256/index.js'; +export * from './sha512/index.js'; export * from './pedersen/index.js'; export * from './poseidon/index.js'; diff --git a/yarn-project/foundation/src/crypto/sha512/index.ts b/yarn-project/foundation/src/crypto/sha512/index.ts new file mode 100644 index 00000000000..d672c0be919 --- /dev/null +++ b/yarn-project/foundation/src/crypto/sha512/index.ts @@ -0,0 +1,16 @@ +import { default as hash } from 'hash.js'; + +import { GrumpkinScalar } from '../../fields/fields.js'; +import { type Bufferable, serializeToBuffer } from '../../serialize/serialize.js'; + +export const sha512 = (data: Buffer) => Buffer.from(hash.sha512().update(data).digest()); + +/** + * @dev We don't truncate in this function (unlike in sha256ToField) because this function is used in situations where + * we don't care only about collision resistance but we need the output to be uniformly distributed as well. This is + * because we use it as a pseudo-random function. + */ +export const sha512ToGrumpkinScalar = (data: Bufferable[]) => { + const buffer = serializeToBuffer(data); + return GrumpkinScalar.fromBufferReduce(sha512(buffer)); +}; diff --git a/yarn-project/key-store/src/new_test_key_store.test.ts b/yarn-project/key-store/src/new_test_key_store.test.ts new file mode 100644 index 00000000000..4732e0c6ca0 --- /dev/null +++ b/yarn-project/key-store/src/new_test_key_store.test.ts @@ -0,0 +1,42 @@ +import { Fr } from '@aztec/circuits.js'; +import { Grumpkin } from '@aztec/circuits.js/barretenberg'; +import { openTmpStore } from '@aztec/kv-store/utils'; + +import { NewTestKeyStore } from './new_test_key_store.js'; + +describe('NewTestKeyStore', () => { + it('Adds account and returns keys', async () => { + const db = openTmpStore(); + const keyStore = new NewTestKeyStore(new Grumpkin(), db); + + // Arbitrary fixed values + const sk = new Fr(8923n); + const partialAddress = new Fr(243523n); + + const accountAddress = await keyStore.addAccount(sk, partialAddress); + expect(accountAddress.toString()).toMatchInlineSnapshot( + `"0x2e34847ad9019320ac89a6ec9b42fec90f94ef4162fdfdd7f5b7668e32d82655"`, + ); + + // TODO(#5714): The keys are currently the same here because separator is currently ignored in poseidon + const masterNullifierPublicKey = await keyStore.getMasterNullifierPublicKey(accountAddress); + expect(masterNullifierPublicKey.toString()).toMatchInlineSnapshot( + `"0x2ef5d15dd65d29546680ab72846fb071f41cb9f2a0212215e6c560e29df4ff650ce764818364b376be92dc2f49577fe440e64a16012584f7c4ee94f7edbc323a"`, + ); + + const masterIncomingViewingPublicKey = await keyStore.getMasterIncomingViewingPublicKey(accountAddress); + expect(masterIncomingViewingPublicKey.toString()).toMatchInlineSnapshot( + `"0x1c088f4e4a711f236a88b55da9ddf388de0bc00d56a5ceca96cea3a5cbe75bf32db0a333ba30c36b844d9fc6d2fb0de8d10e4371f0c5baebae452d90ff366798"`, + ); + + const masterOutgoingViewingPublicKey = await keyStore.getMasterOutgoingViewingPublicKey(accountAddress); + expect(masterOutgoingViewingPublicKey.toString()).toMatchInlineSnapshot( + `"0x232d0b445d097fbc2046012c3fc474f6a9beef97eda1d8d1f2487dbe501ee1e70e8db9a824531a14e8717dee54cbb7abfec29a88c550a49617258bd6fd858242"`, + ); + + const masterTaggingPublicKey = await keyStore.getMasterTaggingPublicKey(accountAddress); + expect(masterTaggingPublicKey.toString()).toMatchInlineSnapshot( + `"0x076429010fdebfa522b053267f654a4c5daf18589915d96f7e5001d63ea2033f27f915f254560c84450aa38e93c3162be52492d05b316e75f542e3b302117360"`, + ); + }); +}); diff --git a/yarn-project/key-store/src/new_test_key_store.ts b/yarn-project/key-store/src/new_test_key_store.ts new file mode 100644 index 00000000000..5a48036d760 --- /dev/null +++ b/yarn-project/key-store/src/new_test_key_store.ts @@ -0,0 +1,130 @@ +import { type NewKeyStore, type PublicKey } from '@aztec/circuit-types'; +import { AztecAddress, Fr, GeneratorIndex, type PartialAddress, Point } from '@aztec/circuits.js'; +import { type Grumpkin } from '@aztec/circuits.js/barretenberg'; +import { poseidon2Hash, sha512ToGrumpkinScalar } from '@aztec/foundation/crypto'; +import { type AztecKVStore, type AztecMap } from '@aztec/kv-store'; + +/** + * TestKeyStore is an implementation of the KeyStore interface, used for managing key pairs in a testing environment. + * It should be utilized in testing scenarios where secure key management is not required, and ease-of-use is prioritized. + */ +export class NewTestKeyStore implements NewKeyStore { + #keys: AztecMap; + + constructor(private curve: Grumpkin, database: AztecKVStore) { + this.#keys = database.openMap('key_store'); + } + + /** + * Creates a new account from a randomly generated secret key. + * @returns A promise that resolves to the newly created account's AztecAddress. + */ + public createAccount(): Promise { + const sk = Fr.random(); + const partialAddress = Fr.random(); + return this.addAccount(sk, partialAddress); + } + + /** + * Adds an account to the key store from the provided secret key. + * @param sk - The secret key of the account. + * @param partialAddress - The partial address of the account. + * @returns The account's address. + */ + public async addAccount(sk: Fr, partialAddress: PartialAddress): Promise { + // First we derive master secret keys - we use sha512 here because this derivation will never take place + // in a circuit + const masterNullifierSecretKey = sha512ToGrumpkinScalar([sk, GeneratorIndex.NSK_M]); + const masterIncomingViewingSecretKey = sha512ToGrumpkinScalar([sk, GeneratorIndex.IVSK_M]); + const masterOutgoingViewingSecretKey = sha512ToGrumpkinScalar([sk, GeneratorIndex.OVSK_M]); + const masterTaggingSecretKey = sha512ToGrumpkinScalar([sk, GeneratorIndex.TSK_M]); + + // Then we derive master public keys + const masterNullifierPublicKey = this.curve.mul(this.curve.generator(), masterNullifierSecretKey); + const masterIncomingViewingPublicKey = this.curve.mul(this.curve.generator(), masterIncomingViewingSecretKey); + const masterOutgoingViewingPublicKey = this.curve.mul(this.curve.generator(), masterOutgoingViewingSecretKey); + const masterTaggingPublicKey = this.curve.mul(this.curve.generator(), masterTaggingSecretKey); + + // We hash the public keys to get the public keys hash + const publicKeysHash = poseidon2Hash( + [ + masterNullifierPublicKey, + masterIncomingViewingPublicKey, + masterOutgoingViewingPublicKey, + masterTaggingPublicKey, + ], + GeneratorIndex.PUBLIC_KEYS_HASH, + ); + + // We hash the partial address and the public keys hash to get the account address + // TODO(#5726): Should GeneratorIndex.CONTRACT_ADDRESS be removed given that we introduced CONTRACT_ADDRESS_V1? + // TODO(#5726): Move the following line to AztecAddress class? + const accountAddressFr = poseidon2Hash([partialAddress, publicKeysHash], GeneratorIndex.CONTRACT_ADDRESS_V1); + const accountAddress = AztecAddress.fromField(accountAddressFr); + + // We store the keys in the database + await this.#keys.set(`${accountAddress.toString()}-npk_m`, masterNullifierPublicKey.toBuffer()); + await this.#keys.set(`${accountAddress.toString()}-ivpk_m`, masterIncomingViewingPublicKey.toBuffer()); + await this.#keys.set(`${accountAddress.toString()}-ovpk_m`, masterOutgoingViewingPublicKey.toBuffer()); + await this.#keys.set(`${accountAddress.toString()}-tpk_m`, masterTaggingPublicKey.toBuffer()); + + // At last, we return the newly derived account address + return Promise.resolve(accountAddress); + } + + /** + * Gets the master nullifier public key for a given account. + * @throws If the account does not exist in the key store. + * @param account - The account address for which to retrieve the master nullifier public key. + * @returns The master nullifier public key for the account. + */ + public getMasterNullifierPublicKey(account: AztecAddress): Promise { + const masterNullifierPublicKeyBuffer = this.#keys.get(`${account.toString()}-npk_m`); + if (!masterNullifierPublicKeyBuffer) { + throw new Error(`Account ${account.toString()} does not exist.`); + } + return Promise.resolve(Point.fromBuffer(masterNullifierPublicKeyBuffer)); + } + + /** + * Gets the master incoming viewing public key for a given account. + * @throws If the account does not exist in the key store. + * @param account - The account address for which to retrieve the master incoming viewing public key. + * @returns The master incoming viewing public key for the account. + */ + public getMasterIncomingViewingPublicKey(account: AztecAddress): Promise { + const masterIncomingViewingPublicKeyBuffer = this.#keys.get(`${account.toString()}-ivpk_m`); + if (!masterIncomingViewingPublicKeyBuffer) { + throw new Error(`Account ${account.toString()} does not exist.`); + } + return Promise.resolve(Point.fromBuffer(masterIncomingViewingPublicKeyBuffer)); + } + + /** + * Retrieves the master outgoing viewing public key. + * @throws If the account does not exist in the key store. + * @param account - The account to retrieve the master outgoing viewing key for. + * @returns A Promise that resolves to the master outgoing viewing key. + */ + public getMasterOutgoingViewingPublicKey(account: AztecAddress): Promise { + const masterOutgoingViewingPublicKeyBuffer = this.#keys.get(`${account.toString()}-ovpk_m`); + if (!masterOutgoingViewingPublicKeyBuffer) { + throw new Error(`Account ${account.toString()} does not exist.`); + } + return Promise.resolve(Point.fromBuffer(masterOutgoingViewingPublicKeyBuffer)); + } + + /** + * Retrieves the master tagging public key. + * @throws If the account does not exist in the key store. + * @param account - The account to retrieve the master tagging key for. + * @returns A Promise that resolves to the master tagging key. + */ + public getMasterTaggingPublicKey(account: AztecAddress): Promise { + const masterTaggingPublicKeyBuffer = this.#keys.get(`${account.toString()}-tpk_m`); + if (!masterTaggingPublicKeyBuffer) { + throw new Error(`Account ${account.toString()} does not exist.`); + } + return Promise.resolve(Point.fromBuffer(masterTaggingPublicKeyBuffer)); + } +} diff --git a/yarn-project/key-store/src/test_key_store.ts b/yarn-project/key-store/src/test_key_store.ts index a5bfe3144b3..4af7e2c2dfa 100644 --- a/yarn-project/key-store/src/test_key_store.ts +++ b/yarn-project/key-store/src/test_key_store.ts @@ -16,6 +16,7 @@ import { ConstantKeyPair } from './key_pair.js'; /** * TestKeyStore is an implementation of the KeyStore interface, used for managing key pairs in a testing environment. * It should be utilized in testing scenarios where secure key management is not required, and ease-of-use is prioritized. + * TODO(#5627): 💣💣💣 */ export class TestKeyStore implements KeyStore { #keys: AztecMap;