@aztec-labs/key-store 6.0.0-nightly.20260829

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # Key Store
2
+
3
+ A key store is an input component for the [PXE](../pxe/) to manage keys securely. It provides a secure environment and interfaces for users to manage their keys. When the PXE requests keys and signatures from the key store, a well-designed key store should prompt users to authorize the requested action before sending any sensitive information to the PXE. This helps to ensure that sensitive data, such as private keys, is not accessible to unauthorized parties. Additionally, the key store should provide robust protection mechanisms such as encryption, multi-factor authentication, and backup/restore functionalities to prevent data loss or theft.
@@ -0,0 +1,23 @@
1
+ import type { GrumpkinScalar } from '@aztec-labs/foundation/curves/grumpkin';
2
+ import type { PublicKey } from '@aztec-labs/stdlib/keys';
3
+ /**
4
+ * The four master privacy secret keys the key store holds for an account: the nullifier-hiding, incoming-viewing,
5
+ * outgoing-viewing, and tagging keys.
6
+ */
7
+ export type AccountPrivacySecretKeys = {
8
+ masterNullifierHidingSecretKey: GrumpkinScalar;
9
+ masterIncomingViewingSecretKey: GrumpkinScalar;
10
+ masterOutgoingViewingSecretKey: GrumpkinScalar;
11
+ masterTaggingSecretKey: GrumpkinScalar;
12
+ };
13
+ /**
14
+ * The keys needed to register an account: the four privacy secret keys the key store holds, plus the *public*
15
+ * message-signing and fallback keys. The message-signing and fallback secret keys are withheld from the key store (and
16
+ * hence from PXE, which embeds it), since it is not trusted to hold them: only their public keys are needed (to
17
+ * reconstruct the account's address).
18
+ */
19
+ export type AccountPrivacyKeys = AccountPrivacySecretKeys & {
20
+ masterMessageSigningPublicKey: PublicKey;
21
+ masterFallbackPublicKey: PublicKey;
22
+ };
23
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYWNjb3VudF9wcml2YWN5X2tleXMuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9hY2NvdW50X3ByaXZhY3lfa2V5cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssRUFBRSxjQUFjLEVBQUUsTUFBTSx3Q0FBd0MsQ0FBQztBQUM3RSxPQUFPLEtBQUssRUFBRSxTQUFTLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUV6RDs7O0dBR0c7QUFDSCxNQUFNLE1BQU0sd0JBQXdCLEdBQUc7SUFDckMsOEJBQThCLEVBQUUsY0FBYyxDQUFDO0lBQy9DLDhCQUE4QixFQUFFLGNBQWMsQ0FBQztJQUMvQyw4QkFBOEIsRUFBRSxjQUFjLENBQUM7SUFDL0Msc0JBQXNCLEVBQUUsY0FBYyxDQUFDO0NBQ3hDLENBQUM7QUFFRjs7Ozs7R0FLRztBQUNILE1BQU0sTUFBTSxrQkFBa0IsR0FBRyx3QkFBd0IsR0FBRztJQUMxRCw2QkFBNkIsRUFBRSxTQUFTLENBQUM7SUFDekMsdUJBQXVCLEVBQUUsU0FBUyxDQUFDO0NBQ3BDLENBQUMifQ==
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account_privacy_keys.d.ts","sourceRoot":"","sources":["../src/account_privacy_keys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wCAAwC,CAAC;AAC7E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD;;;GAGG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,8BAA8B,EAAE,cAAc,CAAC;IAC/C,8BAA8B,EAAE,cAAc,CAAC;IAC/C,8BAA8B,EAAE,cAAc,CAAC;IAC/C,sBAAsB,EAAE,cAAc,CAAC;CACxC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,wBAAwB,GAAG;IAC1D,6BAA6B,EAAE,SAAS,CAAC;IACzC,uBAAuB,EAAE,SAAS,CAAC;CACpC,CAAC"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The keys needed to register an account: the four privacy secret keys the key store holds, plus the *public*
3
+ * message-signing and fallback keys. The message-signing and fallback secret keys are withheld from the key store (and
4
+ * hence from PXE, which embeds it), since it is not trusted to hold them: only their public keys are needed (to
5
+ * reconstruct the account's address).
6
+ */ export { };
@@ -0,0 +1,3 @@
1
+ export * from './account_privacy_keys.js';
2
+ export * from './key_store.js';
3
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxjQUFjLDJCQUEyQixDQUFDO0FBQzFDLGNBQWMsZ0JBQWdCLENBQUMifQ==
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,2BAA2B,CAAC;AAC1C,cAAc,gBAAgB,CAAC"}
package/dest/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './account_privacy_keys.js';
2
+ export * from './key_store.js';
@@ -0,0 +1,110 @@
1
+ import { Fr } from '@aztec-labs/foundation/curves/bn254';
2
+ import { GrumpkinScalar } from '@aztec-labs/foundation/curves/grumpkin';
3
+ import { type Bufferable } from '@aztec-labs/foundation/serialize';
4
+ import type { AztecAsyncKVStore } from '@aztec-labs/kv-store';
5
+ import { AztecAddress } from '@aztec-labs/stdlib/aztec-address';
6
+ import { CompleteAddress, type PartialAddress } from '@aztec-labs/stdlib/contract';
7
+ import { KeyValidationRequest } from '@aztec-labs/stdlib/kernel';
8
+ import { type KeyPrefix, type PublicKey } from '@aztec-labs/stdlib/keys';
9
+ import type { AccountPrivacyKeys, AccountPrivacySecretKeys } from './account_privacy_keys.js';
10
+ /**
11
+ * Used for managing keys. Can hold keys of multiple accounts.
12
+ */
13
+ export declare class KeyStore {
14
+ #private;
15
+ static readonly SCHEMA_VERSION = 1;
16
+ constructor(database: AztecAsyncKVStore);
17
+ /**
18
+ * Adds an account to the key store.
19
+ *
20
+ * The key store holds the four privacy secret keys (nullifier-hiding, incoming-viewing, outgoing-viewing, tagging),
21
+ * but only the *public* message-signing and fallback keys: their secret keys are withheld, since the key store (and
22
+ * PXE, which embeds it) is not trusted to hold them. The public keys are still needed to reconstruct the account's
23
+ * address, which commits to all six master public keys.
24
+ *
25
+ * @param keys - The account's privacy keys: four secret keys plus the message-signing and fallback public keys.
26
+ * @param partialAddress - The partial address of the account.
27
+ * @returns The account's complete address.
28
+ * @throws If any of the account's six master public keys would be the point at infinity.
29
+ */
30
+ addAccount(keys: AccountPrivacyKeys, partialAddress: PartialAddress): Promise<CompleteAddress>;
31
+ /**
32
+ * Retrieves addresses of accounts stored in the key store.
33
+ * @returns A Promise that resolves to an array of account addresses.
34
+ */
35
+ getAccounts(): Promise<AztecAddress[]>;
36
+ /** Checks whether an account is registered in the key store. */
37
+ hasAccount(account: AztecAddress): Promise<boolean>;
38
+ /**
39
+ * Gets the key validation request for a given master public key hash and contract address.
40
+ * @throws If the account corresponding to the master public key hash does not exist in the key store.
41
+ * @param pkMHash - The master public key hash.
42
+ * @param contractAddress - The contract address to silo the secret key in the key validation request with.
43
+ * @returns The key validation request.
44
+ */
45
+ getKeyValidationRequest(pkMHash: Fr, contractAddress: AztecAddress): Promise<KeyValidationRequest>;
46
+ /**
47
+ * Gets the master nullifier public key for a given account.
48
+ * @throws If the account does not exist in the key store.
49
+ */
50
+ getMasterNullifierHidingPublicKey(account: AztecAddress): Promise<PublicKey>;
51
+ /**
52
+ * Gets the master incoming viewing public key for a given account.
53
+ * @throws If the account does not exist in the key store.
54
+ */
55
+ getMasterIncomingViewingPublicKey(account: AztecAddress): Promise<PublicKey>;
56
+ /**
57
+ * Retrieves the master outgoing viewing public key.
58
+ * @throws If the account does not exist in the key store.
59
+ */
60
+ getMasterOutgoingViewingPublicKey(account: AztecAddress): Promise<PublicKey>;
61
+ /**
62
+ * Retrieves the master tagging public key.
63
+ * @throws If the account does not exist in the key store.
64
+ */
65
+ getMasterTaggingPublicKey(account: AztecAddress): Promise<PublicKey>;
66
+ /**
67
+ * Retrieves master incoming viewing secret key.
68
+ * @throws If the account does not exist in the key store.
69
+ */
70
+ getMasterIncomingViewingSecretKey(account: AztecAddress): Promise<GrumpkinScalar>;
71
+ /**
72
+ * Retrieves the four privacy secret keys the key store holds for an account. Paired with {@link addAccount}, this
73
+ * allows exporting an account's privacy secret keys, e.g. to re-register it on another PXE. The message-signing and
74
+ * fallback secret keys are not held by the key store and so are not returned.
75
+ *
76
+ * @throws If the account does not exist in the key store.
77
+ */
78
+ getAccountSecretKeys(account: AztecAddress): Promise<AccountPrivacySecretKeys>;
79
+ /**
80
+ * Retrieves application outgoing viewing secret key.
81
+ * @throws If the account does not exist in the key store.
82
+ * @param account - The account to retrieve the application outgoing viewing secret key for.
83
+ * @param app - The application address to retrieve the outgoing viewing secret key for.
84
+ * @returns A Promise that resolves to the application outgoing viewing secret key.
85
+ */
86
+ getAppOutgoingViewingSecretKey(account: AztecAddress, app: AztecAddress): Promise<Fr>;
87
+ /**
88
+ * Retrieves the sk_m corresponding to the given pk_m hash.
89
+ * @throws If the provided hash is not associated with any of the registered accounts.
90
+ * @param pkMHash - The master public key hash to get secret key for.
91
+ * @returns A Promise that resolves to sk_m.
92
+ * @dev Used when feeding the sk_m to the kernel circuit for keys verification.
93
+ */
94
+ getMasterSecretKey(pkMHash: Fr): Promise<GrumpkinScalar>;
95
+ /**
96
+ * Checks whether a given account has a key matching the provided master public key hash.
97
+ * @param account - The account address to check.
98
+ * @param pkMHash - The master public key hash to look for.
99
+ * @returns True if the account has a key with the given hash.
100
+ */
101
+ accountHasKey(account: AztecAddress, pkMHash: Fr): Promise<boolean>;
102
+ /**
103
+ * Gets the key prefix and account address for a given value.
104
+ * @returns A tuple containing the key prefix and account address.
105
+ * @dev Note that this is quite inefficient but it should not matter because there should never be too many keys
106
+ * in the key store.
107
+ */
108
+ getKeyPrefixAndAccount(value: Bufferable): Promise<[KeyPrefix, AztecAddress]>;
109
+ }
110
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoia2V5X3N0b3JlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMva2V5X3N0b3JlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUVBLE9BQU8sRUFBRSxFQUFFLEVBQUUsTUFBTSxxQ0FBcUMsQ0FBQztBQUN6RCxPQUFPLEVBQUUsY0FBYyxFQUFTLE1BQU0sd0NBQXdDLENBQUM7QUFFL0UsT0FBTyxFQUFFLEtBQUssVUFBVSxFQUFxQixNQUFNLGtDQUFrQyxDQUFDO0FBQ3RGLE9BQU8sS0FBSyxFQUFFLGlCQUFpQixFQUFpQixNQUFNLHNCQUFzQixDQUFDO0FBQzdFLE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUNoRSxPQUFPLEVBQUUsZUFBZSxFQUFFLEtBQUssY0FBYyxFQUFFLE1BQU0sNkJBQTZCLENBQUM7QUFDbkYsT0FBTyxFQUFFLG9CQUFvQixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFDakUsT0FBTyxFQUVMLEtBQUssU0FBUyxFQUNkLEtBQUssU0FBUyxFQUtmLE1BQU0seUJBQXlCLENBQUM7QUFFakMsT0FBTyxLQUFLLEVBQUUsa0JBQWtCLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQStEOUY7O0dBRUc7QUFDSCxxQkFBYSxRQUFROztJQUNuQixnQkFBdUIsY0FBYyxLQUFLO0lBSTFDLFlBQVksUUFBUSxFQUFFLGlCQUFpQixFQUd0QztJQUVEOzs7Ozs7Ozs7Ozs7T0FZRztJQUNVLFVBQVUsQ0FBQyxJQUFJLEVBQUUsa0JBQWtCLEVBQUUsY0FBYyxFQUFFLGNBQWMsR0FBRyxPQUFPLENBQUMsZUFBZSxDQUFDLENBRzFHO0lBRUQ7OztPQUdHO0lBQ1UsV0FBVyxJQUFJLE9BQU8sQ0FBQyxZQUFZLEVBQUUsQ0FBQyxDQUtsRDtJQUVELGdFQUFnRTtJQUNuRCxVQUFVLENBQUMsT0FBTyxFQUFFLFlBQVksR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBRS9EO0lBRUQ7Ozs7OztPQU1HO0lBQ0ksdUJBQXVCLENBQUMsT0FBTyxFQUFFLEVBQUUsRUFBRSxlQUFlLEVBQUUsWUFBWSxHQUFHLE9BQU8sQ0FBQyxvQkFBb0IsQ0FBQyxDQTBDeEc7SUFFRDs7O09BR0c7SUFDVSxpQ0FBaUMsQ0FBQyxPQUFPLEVBQUUsWUFBWSxHQUFHLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FFeEY7SUFFRDs7O09BR0c7SUFDVSxpQ0FBaUMsQ0FBQyxPQUFPLEVBQUUsWUFBWSxHQUFHLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FFeEY7SUFFRDs7O09BR0c7SUFDVSxpQ0FBaUMsQ0FBQyxPQUFPLEVBQUUsWUFBWSxHQUFHLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FFeEY7SUFFRDs7O09BR0c7SUFDVSx5QkFBeUIsQ0FBQyxPQUFPLEVBQUUsWUFBWSxHQUFHLE9BQU8sQ0FBQyxTQUFTLENBQUMsQ0FFaEY7SUFFRDs7O09BR0c7SUFDVSxpQ0FBaUMsQ0FBQyxPQUFPLEVBQUUsWUFBWSxHQUFHLE9BQU8sQ0FBQyxjQUFjLENBQUMsQ0FFN0Y7SUFFRDs7Ozs7O09BTUc7SUFDVSxvQkFBb0IsQ0FBQyxPQUFPLEVBQUUsWUFBWSxHQUFHLE9BQU8sQ0FBQyx3QkFBd0IsQ0FBQyxDQVExRjtJQUVEOzs7Ozs7T0FNRztJQUNVLDhCQUE4QixDQUFDLE9BQU8sRUFBRSxZQUFZLEVBQUUsR0FBRyxFQUFFLFlBQVksR0FBRyxPQUFPLENBQUMsRUFBRSxDQUFDLENBT2pHO0lBRUQ7Ozs7OztPQU1HO0lBQ0ksa0JBQWtCLENBQUMsT0FBTyxFQUFFLEVBQUUsR0FBRyxPQUFPLENBQUMsY0FBYyxDQUFDLENBNkI5RDtJQUVEOzs7OztPQUtHO0lBQ0ksYUFBYSxDQUFDLE9BQU8sRUFBRSxZQUFZLEVBQUUsT0FBTyxFQUFFLEVBQUUsR0FBRyxPQUFPLENBQUMsT0FBTyxDQUFDLENBV3pFO0lBRUQ7Ozs7O09BS0c7SUFDVSxzQkFBc0IsQ0FBQyxLQUFLLEVBQUUsVUFBVSxHQUFHLE9BQU8sQ0FBQyxDQUFDLFNBQVMsRUFBRSxZQUFZLENBQUMsQ0FBQyxDQWN6RjtDQWdGRiJ9
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key_store.d.ts","sourceRoot":"","sources":["../src/key_store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,EAAE,EAAE,MAAM,qCAAqC,CAAC;AACzD,OAAO,EAAE,cAAc,EAAS,MAAM,wCAAwC,CAAC;AAE/E,OAAO,EAAE,KAAK,UAAU,EAAqB,MAAM,kCAAkC,CAAC;AACtF,OAAO,KAAK,EAAE,iBAAiB,EAAiB,MAAM,sBAAsB,CAAC;AAC7E,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAChE,OAAO,EAAE,eAAe,EAAE,KAAK,cAAc,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAEL,KAAK,SAAS,EACd,KAAK,SAAS,EAKf,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AA+D9F;;GAEG;AACH,qBAAa,QAAQ;;IACnB,gBAAuB,cAAc,KAAK;IAI1C,YAAY,QAAQ,EAAE,iBAAiB,EAGtC;IAED;;;;;;;;;;;;OAYG;IACU,UAAU,CAAC,IAAI,EAAE,kBAAkB,EAAE,cAAc,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC,CAG1G;IAED;;;OAGG;IACU,WAAW,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAKlD;IAED,gEAAgE;IACnD,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAAC,CAE/D;IAED;;;;;;OAMG;IACI,uBAAuB,CAAC,OAAO,EAAE,EAAE,EAAE,eAAe,EAAE,YAAY,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA0CxG;IAED;;;OAGG;IACU,iCAAiC,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,CAExF;IAED;;;OAGG;IACU,iCAAiC,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,CAExF;IAED;;;OAGG;IACU,iCAAiC,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,CAExF;IAED;;;OAGG;IACU,yBAAyB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,SAAS,CAAC,CAEhF;IAED;;;OAGG;IACU,iCAAiC,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC,CAE7F;IAED;;;;;;OAMG;IACU,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAQ1F;IAED;;;;;;OAMG;IACU,8BAA8B,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,EAAE,CAAC,CAOjG;IAED;;;;;;OAMG;IACI,kBAAkB,CAAC,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CA6B9D;IAED;;;;;OAKG;IACI,aAAa,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAWzE;IAED;;;;;OAKG;IACU,sBAAsB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAczF;CAgFF"}
@@ -0,0 +1,308 @@
1
+ import { DomainSeparator } from '@aztec-labs/constants';
2
+ import { poseidon2HashWithSeparator } from '@aztec-labs/foundation/crypto/poseidon';
3
+ import { GrumpkinScalar, Point } from '@aztec-labs/foundation/curves/grumpkin';
4
+ import { toArray } from '@aztec-labs/foundation/iterable';
5
+ import { serializeToBuffer } from '@aztec-labs/foundation/serialize';
6
+ import { AztecAddress } from '@aztec-labs/stdlib/aztec-address';
7
+ import { CompleteAddress } from '@aztec-labs/stdlib/contract';
8
+ import { KeyValidationRequest } from '@aztec-labs/stdlib/kernel';
9
+ import { KEY_PREFIXES, PublicKeys, computeAppSecretKey, derivePublicKeyFromSecretKey, hashPublicKey } from '@aztec-labs/stdlib/keys';
10
+ /** Maps a key prefix to the storage suffix for the corresponding master secret key. */ function secretKeyStorageSuffix(prefix) {
11
+ return prefix === 'n' ? 'nhk_m' : `${prefix}sk_m`;
12
+ }
13
+ /**
14
+ * Computes the public counterparts of an account's four privacy secret keys and assembles its {@link PublicKeys} struct
15
+ * (used to derive the address), from the {@link AccountPrivacyKeys} passed to {@link KeyStore.addAccount}.
16
+ *
17
+ * The message-signing and fallback keys are already supplied as public keys, since the key store never holds their
18
+ * secrets.
19
+ */ async function completeAccountKeys(keys) {
20
+ const { masterNullifierHidingSecretKey, masterIncomingViewingSecretKey, masterOutgoingViewingSecretKey, masterTaggingSecretKey, masterMessageSigningPublicKey, masterFallbackPublicKey } = keys;
21
+ const masterNullifierHidingPublicKey = await derivePublicKeyFromSecretKey(masterNullifierHidingSecretKey);
22
+ const masterIncomingViewingPublicKey = await derivePublicKeyFromSecretKey(masterIncomingViewingSecretKey);
23
+ const masterOutgoingViewingPublicKey = await derivePublicKeyFromSecretKey(masterOutgoingViewingSecretKey);
24
+ const masterTaggingPublicKey = await derivePublicKeyFromSecretKey(masterTaggingSecretKey);
25
+ for (const [name, publicKey] of Object.entries({
26
+ masterNullifierHidingPublicKey,
27
+ masterIncomingViewingPublicKey,
28
+ masterOutgoingViewingPublicKey,
29
+ masterTaggingPublicKey,
30
+ masterMessageSigningPublicKey,
31
+ masterFallbackPublicKey
32
+ })){
33
+ if (publicKey.isInfinite) {
34
+ throw new Error(`Cannot register an account with an infinity ${name}.`);
35
+ }
36
+ }
37
+ const publicKeys = new PublicKeys(await hashPublicKey(masterNullifierHidingPublicKey), masterIncomingViewingPublicKey, await hashPublicKey(masterOutgoingViewingPublicKey), await hashPublicKey(masterTaggingPublicKey), await hashPublicKey(masterMessageSigningPublicKey), await hashPublicKey(masterFallbackPublicKey));
38
+ return {
39
+ masterNullifierHidingSecretKey,
40
+ masterIncomingViewingSecretKey,
41
+ masterOutgoingViewingSecretKey,
42
+ masterTaggingSecretKey,
43
+ masterNullifierHidingPublicKey,
44
+ masterOutgoingViewingPublicKey,
45
+ masterTaggingPublicKey,
46
+ publicKeys
47
+ };
48
+ }
49
+ /**
50
+ * Used for managing keys. Can hold keys of multiple accounts.
51
+ */ export class KeyStore {
52
+ static SCHEMA_VERSION = 1;
53
+ #db;
54
+ #keys;
55
+ constructor(database){
56
+ this.#db = database;
57
+ this.#keys = database.openMap('key_store');
58
+ }
59
+ /**
60
+ * Adds an account to the key store.
61
+ *
62
+ * The key store holds the four privacy secret keys (nullifier-hiding, incoming-viewing, outgoing-viewing, tagging),
63
+ * but only the *public* message-signing and fallback keys: their secret keys are withheld, since the key store (and
64
+ * PXE, which embeds it) is not trusted to hold them. The public keys are still needed to reconstruct the account's
65
+ * address, which commits to all six master public keys.
66
+ *
67
+ * @param keys - The account's privacy keys: four secret keys plus the message-signing and fallback public keys.
68
+ * @param partialAddress - The partial address of the account.
69
+ * @returns The account's complete address.
70
+ * @throws If any of the account's six master public keys would be the point at infinity.
71
+ */ async addAccount(keys, partialAddress) {
72
+ const accountKeys = await completeAccountKeys(keys);
73
+ return this.#storeAccountKeys(accountKeys, partialAddress);
74
+ }
75
+ /**
76
+ * Retrieves addresses of accounts stored in the key store.
77
+ * @returns A Promise that resolves to an array of account addresses.
78
+ */ async getAccounts() {
79
+ const allMapKeys = await this.#db.transactionAsync(()=>toArray(this.#keys.keysAsync()));
80
+ // We return account addresses based on the map keys that end with '-ivsk_m'
81
+ const accounts = allMapKeys.filter((key)=>key.endsWith('-ivsk_m')).map((key)=>key.split('-')[0]);
82
+ return accounts.map((account)=>AztecAddress.fromStringUnsafe(account));
83
+ }
84
+ /** Checks whether an account is registered in the key store. */ async hasAccount(account) {
85
+ return !!await this.#db.transactionAsync(()=>this.#keys.getAsync(`${account.toString()}-ivsk_m`));
86
+ }
87
+ /**
88
+ * Gets the key validation request for a given master public key hash and contract address.
89
+ * @throws If the account corresponding to the master public key hash does not exist in the key store.
90
+ * @param pkMHash - The master public key hash.
91
+ * @param contractAddress - The contract address to silo the secret key in the key validation request with.
92
+ * @returns The key validation request.
93
+ */ getKeyValidationRequest(pkMHash, contractAddress) {
94
+ return this.#db.transactionAsync(async ()=>{
95
+ const [keyPrefix, account] = await this.getKeyPrefixAndAccount(pkMHash);
96
+ // Load the stored master public key point. The returned KVR carries only the hash, but we
97
+ // use the point here as a witness for two integrity checks below: (1) it matches the supplied
98
+ // hash, and (2) it matches the value derived from the stored secret key.
99
+ const pkMBuffer = await this.#keys.getAsync(`${account.toString()}-${keyPrefix}pk_m`);
100
+ if (!pkMBuffer) {
101
+ throw new Error(`Could not find ${keyPrefix}pk_m for account ${account.toString()} whose address was successfully obtained with ${keyPrefix}pk_m_hash ${pkMHash.toString()}.`);
102
+ }
103
+ const pkM = Point.fromBuffer(pkMBuffer);
104
+ // Now we find the secret key for the public key
105
+ const skStorageSuffix = secretKeyStorageSuffix(keyPrefix);
106
+ const skMBuffer = await this.#keys.getAsync(`${account.toString()}-${skStorageSuffix}`);
107
+ if (!skMBuffer) {
108
+ throw new Error(`Could not find ${skStorageSuffix} for account ${account.toString()} whose address was successfully obtained with ${keyPrefix}pk_m_hash ${pkMHash.toString()}.`);
109
+ }
110
+ const skM = GrumpkinScalar.fromBuffer(skMBuffer);
111
+ // The remaining awaits are non-DB computations. They are safe because no further IDB operations follow them.
112
+ const computedPkMHash = await hashPublicKey(pkM);
113
+ if (!computedPkMHash.equals(pkMHash)) {
114
+ throw new Error(`Could not find ${keyPrefix}pkM for ${keyPrefix}pk_m_hash ${pkMHash.toString()}.`);
115
+ }
116
+ const derivedPkM = await derivePublicKeyFromSecretKey(skM);
117
+ if (!derivedPkM.equals(pkM)) {
118
+ throw new Error(`Could not derive ${keyPrefix}pkM from ${keyPrefix}skM.`);
119
+ }
120
+ const skApp = await computeAppSecretKey(skM, contractAddress, keyPrefix);
121
+ return new KeyValidationRequest(pkMHash, skApp);
122
+ });
123
+ }
124
+ /**
125
+ * Gets the master nullifier public key for a given account.
126
+ * @throws If the account does not exist in the key store.
127
+ */ async getMasterNullifierHidingPublicKey(account) {
128
+ return Point.fromBuffer(await this.#getMasterKeyBuffer(account, 'npk_m'));
129
+ }
130
+ /**
131
+ * Gets the master incoming viewing public key for a given account.
132
+ * @throws If the account does not exist in the key store.
133
+ */ async getMasterIncomingViewingPublicKey(account) {
134
+ return Point.fromBuffer(await this.#getMasterKeyBuffer(account, 'ivpk_m'));
135
+ }
136
+ /**
137
+ * Retrieves the master outgoing viewing public key.
138
+ * @throws If the account does not exist in the key store.
139
+ */ async getMasterOutgoingViewingPublicKey(account) {
140
+ return Point.fromBuffer(await this.#getMasterKeyBuffer(account, 'ovpk_m'));
141
+ }
142
+ /**
143
+ * Retrieves the master tagging public key.
144
+ * @throws If the account does not exist in the key store.
145
+ */ async getMasterTaggingPublicKey(account) {
146
+ return Point.fromBuffer(await this.#getMasterKeyBuffer(account, 'tpk_m'));
147
+ }
148
+ /**
149
+ * Retrieves master incoming viewing secret key.
150
+ * @throws If the account does not exist in the key store.
151
+ */ async getMasterIncomingViewingSecretKey(account) {
152
+ return GrumpkinScalar.fromBuffer(await this.#getMasterKeyBuffer(account, 'ivsk_m'));
153
+ }
154
+ /**
155
+ * Retrieves the four privacy secret keys the key store holds for an account. Paired with {@link addAccount}, this
156
+ * allows exporting an account's privacy secret keys, e.g. to re-register it on another PXE. The message-signing and
157
+ * fallback secret keys are not held by the key store and so are not returned.
158
+ *
159
+ * @throws If the account does not exist in the key store.
160
+ */ async getAccountSecretKeys(account) {
161
+ const [nhkM, ivskM, ovskM, tskM] = await this.#getMasterKeyBuffers(account, [
162
+ 'nhk_m',
163
+ 'ivsk_m',
164
+ 'ovsk_m',
165
+ 'tsk_m'
166
+ ]);
167
+ return {
168
+ masterNullifierHidingSecretKey: GrumpkinScalar.fromBuffer(nhkM),
169
+ masterIncomingViewingSecretKey: GrumpkinScalar.fromBuffer(ivskM),
170
+ masterOutgoingViewingSecretKey: GrumpkinScalar.fromBuffer(ovskM),
171
+ masterTaggingSecretKey: GrumpkinScalar.fromBuffer(tskM)
172
+ };
173
+ }
174
+ /**
175
+ * Retrieves application outgoing viewing secret key.
176
+ * @throws If the account does not exist in the key store.
177
+ * @param account - The account to retrieve the application outgoing viewing secret key for.
178
+ * @param app - The application address to retrieve the outgoing viewing secret key for.
179
+ * @returns A Promise that resolves to the application outgoing viewing secret key.
180
+ */ async getAppOutgoingViewingSecretKey(account, app) {
181
+ const masterOutgoingViewingSecretKey = GrumpkinScalar.fromBuffer(await this.#getMasterKeyBuffer(account, 'ovsk_m'));
182
+ return poseidon2HashWithSeparator([
183
+ masterOutgoingViewingSecretKey.hi,
184
+ masterOutgoingViewingSecretKey.lo,
185
+ app
186
+ ], DomainSeparator.OVSK_M);
187
+ }
188
+ /**
189
+ * Retrieves the sk_m corresponding to the given pk_m hash.
190
+ * @throws If the provided hash is not associated with any of the registered accounts.
191
+ * @param pkMHash - The master public key hash to get secret key for.
192
+ * @returns A Promise that resolves to sk_m.
193
+ * @dev Used when feeding the sk_m to the kernel circuit for keys verification.
194
+ */ getMasterSecretKey(pkMHash) {
195
+ return this.#db.transactionAsync(async ()=>{
196
+ const [keyPrefix, account] = await this.getKeyPrefixAndAccount(pkMHash);
197
+ const skStorageSuffix = secretKeyStorageSuffix(keyPrefix);
198
+ const secretKeyBuffer = await this.#keys.getAsync(`${account.toString()}-${skStorageSuffix}`);
199
+ if (!secretKeyBuffer) {
200
+ throw new Error(`Could not find ${skStorageSuffix} for ${keyPrefix}pk_m_hash ${pkMHash.toString()}. This should not happen.`);
201
+ }
202
+ const skM = GrumpkinScalar.fromBuffer(secretKeyBuffer);
203
+ // Non-DB computation — safe because no further IDB operations follow.
204
+ // Integrity check: confirm the stored secret key still derives the requested hash. The check
205
+ // is hash-based rather than point-equal because the on-disk identifier is `pk_m_hash`;
206
+ // cryptographic collision resistance of `hashPublicKey` makes this equivalent to a
207
+ // direct point comparison in practice.
208
+ const derivedPkM = await derivePublicKeyFromSecretKey(skM);
209
+ const derivedPkMHash = await hashPublicKey(derivedPkM);
210
+ if (!derivedPkMHash.equals(pkMHash)) {
211
+ throw new Error(`Could not find ${skStorageSuffix} for ${keyPrefix}pk_m_hash ${pkMHash.toString()} in secret keys buffer.`);
212
+ }
213
+ return skM;
214
+ });
215
+ }
216
+ /**
217
+ * Checks whether a given account has a key matching the provided master public key hash.
218
+ * @param account - The account address to check.
219
+ * @param pkMHash - The master public key hash to look for.
220
+ * @returns True if the account has a key with the given hash.
221
+ */ accountHasKey(account, pkMHash) {
222
+ return this.#db.transactionAsync(async ()=>{
223
+ const pkMHashBuffer = serializeToBuffer(pkMHash);
224
+ for (const prefix of KEY_PREFIXES){
225
+ const stored = await this.#keys.getAsync(`${account.toString()}-${prefix}pk_m_hash`);
226
+ if (stored && Buffer.from(stored).equals(pkMHashBuffer)) {
227
+ return true;
228
+ }
229
+ }
230
+ return false;
231
+ });
232
+ }
233
+ /**
234
+ * Gets the key prefix and account address for a given value.
235
+ * @returns A tuple containing the key prefix and account address.
236
+ * @dev Note that this is quite inefficient but it should not matter because there should never be too many keys
237
+ * in the key store.
238
+ */ async getKeyPrefixAndAccount(value) {
239
+ const valueBuffer = serializeToBuffer(value);
240
+ for await (const [key, val] of this.#keys.entriesAsync()){
241
+ // Browser returns Uint8Array, Node.js returns Buffer
242
+ if (Buffer.from(val).equals(valueBuffer)) {
243
+ for (const prefix of KEY_PREFIXES){
244
+ if (key.includes(`-${prefix}`)) {
245
+ const account = AztecAddress.fromStringUnsafe(key.split('-')[0]);
246
+ return [
247
+ prefix,
248
+ account
249
+ ];
250
+ }
251
+ }
252
+ }
253
+ }
254
+ throw new Error(`Could not find key prefix.`);
255
+ }
256
+ /**
257
+ * Persists a completed set of account keys and returns the resulting complete address.
258
+ */ async #storeAccountKeys(accountKeys, partialAddress) {
259
+ const { masterNullifierHidingSecretKey, masterIncomingViewingSecretKey, masterOutgoingViewingSecretKey, masterTaggingSecretKey, masterNullifierHidingPublicKey, masterOutgoingViewingPublicKey, masterTaggingPublicKey, publicKeys } = accountKeys;
260
+ const completeAddress = await CompleteAddress.fromPublicKeysAndPartialAddress(publicKeys, partialAddress);
261
+ const { address: account } = completeAddress;
262
+ // completeAccountKeys has already guaranteed these master public keys are non-infinity, which the kernel cannot
263
+ // check but the address relies on.
264
+ // The npk/ovpk/tpk hashes are already in publicKeys; ivpk_m_hash is computed for indexing.
265
+ const masterIncomingViewingPublicKeyHash = await hashPublicKey(publicKeys.ivpkM);
266
+ await this.#db.transactionAsync(async ()=>{
267
+ // Naming of keys is as follows ${account}-${n/iv/ov/t}${sk/pk}_m.
268
+ //
269
+ // The message-signing and fallback keys are not stored: their secret keys are withheld from the key store, and
270
+ // their public keys are only needed transiently to compute the address (they live in the AddressStore).
271
+ await this.#keys.set(`${account.toString()}-ivsk_m`, masterIncomingViewingSecretKey.toBuffer());
272
+ await this.#keys.set(`${account.toString()}-ovsk_m`, masterOutgoingViewingSecretKey.toBuffer());
273
+ await this.#keys.set(`${account.toString()}-tsk_m`, masterTaggingSecretKey.toBuffer());
274
+ await this.#keys.set(`${account.toString()}-nhk_m`, masterNullifierHidingSecretKey.toBuffer());
275
+ await this.#keys.set(`${account.toString()}-npk_m`, masterNullifierHidingPublicKey.toBuffer());
276
+ await this.#keys.set(`${account.toString()}-ivpk_m`, publicKeys.ivpkM.toBuffer());
277
+ await this.#keys.set(`${account.toString()}-ovpk_m`, masterOutgoingViewingPublicKey.toBuffer());
278
+ await this.#keys.set(`${account.toString()}-tpk_m`, masterTaggingPublicKey.toBuffer());
279
+ // We store pk_m_hash under `account-{n/iv/ov/t}pk_m_hash` key to be able to obtain address and key prefix
280
+ // using the #getKeyPrefixAndAccount function later on
281
+ await this.#keys.set(`${account.toString()}-npk_m_hash`, publicKeys.npkMHash.toBuffer());
282
+ await this.#keys.set(`${account.toString()}-ivpk_m_hash`, masterIncomingViewingPublicKeyHash.toBuffer());
283
+ await this.#keys.set(`${account.toString()}-ovpk_m_hash`, publicKeys.ovpkMHash.toBuffer());
284
+ await this.#keys.set(`${account.toString()}-tpk_m_hash`, publicKeys.tpkMHash.toBuffer());
285
+ });
286
+ return completeAddress;
287
+ }
288
+ /**
289
+ * Fetches a stored master key buffer for an account by its storage suffix (e.g. `npk_m`, `ivsk_m`).
290
+ * @throws If the account does not exist in the key store.
291
+ */ async #getMasterKeyBuffer(account, suffix) {
292
+ const [buffer] = await this.#getMasterKeyBuffers(account, [
293
+ suffix
294
+ ]);
295
+ return buffer;
296
+ }
297
+ /**
298
+ * Fetches multiple stored master key buffers for an account in a single transaction, returning them in the order of
299
+ * the requested storage suffixes.
300
+ * @throws If any of the keys is missing (i.e. the account does not exist in the key store).
301
+ */ async #getMasterKeyBuffers(account, suffixes) {
302
+ const buffers = await this.#db.transactionAsync(()=>Promise.all(suffixes.map((suffix)=>this.#keys.getAsync(`${account.toString()}-${suffix}`))));
303
+ if (!buffers.every((buffer)=>buffer !== undefined)) {
304
+ throw new Error(`Account ${account.toString()} does not exist. Registered accounts: ${await this.getAccounts()}.`);
305
+ }
306
+ return buffers;
307
+ }
308
+ }
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "@aztec-labs/key-store",
3
+ "version": "6.0.0-nightly.20260829",
4
+ "type": "module",
5
+ "exports": "./dest/index.js",
6
+ "typedocOptions": {
7
+ "entryPoints": [
8
+ "./src/index.ts"
9
+ ],
10
+ "name": "KeyStore",
11
+ "tsconfig": "./tsconfig.json"
12
+ },
13
+ "scripts": {
14
+ "build": "yarn clean && ../scripts/tsc.sh",
15
+ "build:dev": "../scripts/tsc.sh --watch",
16
+ "clean": "rm -rf ./dest .tsbuildinfo",
17
+ "test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
18
+ },
19
+ "inherits": [
20
+ "../package.common.json"
21
+ ],
22
+ "jest": {
23
+ "moduleNameMapper": {
24
+ "^(\\.{1,2}/.*)\\.[cm]?js$": "$1"
25
+ },
26
+ "testRegex": "./src/.*\\.test\\.(js|mjs|ts)$",
27
+ "rootDir": "./src",
28
+ "transform": {
29
+ "^.+\\.tsx?$": [
30
+ "@swc/jest",
31
+ {
32
+ "jsc": {
33
+ "parser": {
34
+ "syntax": "typescript",
35
+ "decorators": true
36
+ },
37
+ "transform": {
38
+ "decoratorVersion": "2022-03"
39
+ }
40
+ }
41
+ }
42
+ ]
43
+ },
44
+ "extensionsToTreatAsEsm": [
45
+ ".ts"
46
+ ],
47
+ "reporters": [
48
+ "default"
49
+ ],
50
+ "testTimeout": 120000,
51
+ "setupFiles": [
52
+ "../../foundation/src/jest/setup.mjs"
53
+ ],
54
+ "testEnvironment": "../../foundation/src/jest/env.mjs",
55
+ "setupFilesAfterEnv": [
56
+ "../../foundation/src/jest/setupAfterEnv.mjs"
57
+ ]
58
+ },
59
+ "dependencies": {
60
+ "@aztec-labs/constants": "6.0.0-nightly.20260829",
61
+ "@aztec-labs/foundation": "6.0.0-nightly.20260829",
62
+ "@aztec-labs/kv-store": "6.0.0-nightly.20260829",
63
+ "@aztec-labs/stdlib": "6.0.0-nightly.20260829",
64
+ "tslib": "^2.4.0"
65
+ },
66
+ "devDependencies": {
67
+ "@jest/globals": "^30.0.0",
68
+ "@types/jest": "^30.0.0",
69
+ "@types/node": "^22.15.17",
70
+ "@typescript/native-preview": "7.0.0-dev.20260113.1",
71
+ "jest": "^30.0.0",
72
+ "ts-node": "^10.9.1",
73
+ "typescript": "^5.3.3"
74
+ },
75
+ "files": [
76
+ "dest",
77
+ "src",
78
+ "!*.test.*"
79
+ ],
80
+ "types": "./dest/index.d.ts",
81
+ "engines": {
82
+ "node": ">=20.10"
83
+ }
84
+ }
@@ -0,0 +1,24 @@
1
+ import type { GrumpkinScalar } from '@aztec-labs/foundation/curves/grumpkin';
2
+ import type { PublicKey } from '@aztec-labs/stdlib/keys';
3
+
4
+ /**
5
+ * The four master privacy secret keys the key store holds for an account: the nullifier-hiding, incoming-viewing,
6
+ * outgoing-viewing, and tagging keys.
7
+ */
8
+ export type AccountPrivacySecretKeys = {
9
+ masterNullifierHidingSecretKey: GrumpkinScalar;
10
+ masterIncomingViewingSecretKey: GrumpkinScalar;
11
+ masterOutgoingViewingSecretKey: GrumpkinScalar;
12
+ masterTaggingSecretKey: GrumpkinScalar;
13
+ };
14
+
15
+ /**
16
+ * The keys needed to register an account: the four privacy secret keys the key store holds, plus the *public*
17
+ * message-signing and fallback keys. The message-signing and fallback secret keys are withheld from the key store (and
18
+ * hence from PXE, which embeds it), since it is not trusted to hold them: only their public keys are needed (to
19
+ * reconstruct the account's address).
20
+ */
21
+ export type AccountPrivacyKeys = AccountPrivacySecretKeys & {
22
+ masterMessageSigningPublicKey: PublicKey;
23
+ masterFallbackPublicKey: PublicKey;
24
+ };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './account_privacy_keys.js';
2
+ export * from './key_store.js';
@@ -0,0 +1,412 @@
1
+ import { DomainSeparator } from '@aztec-labs/constants';
2
+ import { poseidon2HashWithSeparator } from '@aztec-labs/foundation/crypto/poseidon';
3
+ import { Fr } from '@aztec-labs/foundation/curves/bn254';
4
+ import { GrumpkinScalar, Point } from '@aztec-labs/foundation/curves/grumpkin';
5
+ import { toArray } from '@aztec-labs/foundation/iterable';
6
+ import { type Bufferable, serializeToBuffer } from '@aztec-labs/foundation/serialize';
7
+ import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec-labs/kv-store';
8
+ import { AztecAddress } from '@aztec-labs/stdlib/aztec-address';
9
+ import { CompleteAddress, type PartialAddress } from '@aztec-labs/stdlib/contract';
10
+ import { KeyValidationRequest } from '@aztec-labs/stdlib/kernel';
11
+ import {
12
+ KEY_PREFIXES,
13
+ type KeyPrefix,
14
+ type PublicKey,
15
+ PublicKeys,
16
+ computeAppSecretKey,
17
+ derivePublicKeyFromSecretKey,
18
+ hashPublicKey,
19
+ } from '@aztec-labs/stdlib/keys';
20
+
21
+ import type { AccountPrivacyKeys, AccountPrivacySecretKeys } from './account_privacy_keys.js';
22
+
23
+ /** Maps a key prefix to the storage suffix for the corresponding master secret key. */
24
+ function secretKeyStorageSuffix(prefix: KeyPrefix): string {
25
+ return prefix === 'n' ? 'nhk_m' : `${prefix}sk_m`;
26
+ }
27
+
28
+ /**
29
+ * Computes the public counterparts of an account's four privacy secret keys and assembles its {@link PublicKeys} struct
30
+ * (used to derive the address), from the {@link AccountPrivacyKeys} passed to {@link KeyStore.addAccount}.
31
+ *
32
+ * The message-signing and fallback keys are already supplied as public keys, since the key store never holds their
33
+ * secrets.
34
+ */
35
+ async function completeAccountKeys(keys: AccountPrivacyKeys) {
36
+ const {
37
+ masterNullifierHidingSecretKey,
38
+ masterIncomingViewingSecretKey,
39
+ masterOutgoingViewingSecretKey,
40
+ masterTaggingSecretKey,
41
+ masterMessageSigningPublicKey,
42
+ masterFallbackPublicKey,
43
+ } = keys;
44
+
45
+ const masterNullifierHidingPublicKey = await derivePublicKeyFromSecretKey(masterNullifierHidingSecretKey);
46
+ const masterIncomingViewingPublicKey = await derivePublicKeyFromSecretKey(masterIncomingViewingSecretKey);
47
+ const masterOutgoingViewingPublicKey = await derivePublicKeyFromSecretKey(masterOutgoingViewingSecretKey);
48
+ const masterTaggingPublicKey = await derivePublicKeyFromSecretKey(masterTaggingSecretKey);
49
+
50
+ for (const [name, publicKey] of Object.entries({
51
+ masterNullifierHidingPublicKey,
52
+ masterIncomingViewingPublicKey,
53
+ masterOutgoingViewingPublicKey,
54
+ masterTaggingPublicKey,
55
+ masterMessageSigningPublicKey,
56
+ masterFallbackPublicKey,
57
+ })) {
58
+ if (publicKey.isInfinite) {
59
+ throw new Error(`Cannot register an account with an infinity ${name}.`);
60
+ }
61
+ }
62
+
63
+ const publicKeys = new PublicKeys(
64
+ await hashPublicKey(masterNullifierHidingPublicKey),
65
+ masterIncomingViewingPublicKey,
66
+ await hashPublicKey(masterOutgoingViewingPublicKey),
67
+ await hashPublicKey(masterTaggingPublicKey),
68
+ await hashPublicKey(masterMessageSigningPublicKey),
69
+ await hashPublicKey(masterFallbackPublicKey),
70
+ );
71
+
72
+ return {
73
+ masterNullifierHidingSecretKey,
74
+ masterIncomingViewingSecretKey,
75
+ masterOutgoingViewingSecretKey,
76
+ masterTaggingSecretKey,
77
+ masterNullifierHidingPublicKey,
78
+ masterOutgoingViewingPublicKey,
79
+ masterTaggingPublicKey,
80
+ publicKeys,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Used for managing keys. Can hold keys of multiple accounts.
86
+ */
87
+ export class KeyStore {
88
+ public static readonly SCHEMA_VERSION = 1;
89
+ #db: AztecAsyncKVStore;
90
+ #keys: AztecAsyncMap<string, Buffer>;
91
+
92
+ constructor(database: AztecAsyncKVStore) {
93
+ this.#db = database;
94
+ this.#keys = database.openMap('key_store');
95
+ }
96
+
97
+ /**
98
+ * Adds an account to the key store.
99
+ *
100
+ * The key store holds the four privacy secret keys (nullifier-hiding, incoming-viewing, outgoing-viewing, tagging),
101
+ * but only the *public* message-signing and fallback keys: their secret keys are withheld, since the key store (and
102
+ * PXE, which embeds it) is not trusted to hold them. The public keys are still needed to reconstruct the account's
103
+ * address, which commits to all six master public keys.
104
+ *
105
+ * @param keys - The account's privacy keys: four secret keys plus the message-signing and fallback public keys.
106
+ * @param partialAddress - The partial address of the account.
107
+ * @returns The account's complete address.
108
+ * @throws If any of the account's six master public keys would be the point at infinity.
109
+ */
110
+ public async addAccount(keys: AccountPrivacyKeys, partialAddress: PartialAddress): Promise<CompleteAddress> {
111
+ const accountKeys = await completeAccountKeys(keys);
112
+ return this.#storeAccountKeys(accountKeys, partialAddress);
113
+ }
114
+
115
+ /**
116
+ * Retrieves addresses of accounts stored in the key store.
117
+ * @returns A Promise that resolves to an array of account addresses.
118
+ */
119
+ public async getAccounts(): Promise<AztecAddress[]> {
120
+ const allMapKeys = await this.#db.transactionAsync(() => toArray(this.#keys.keysAsync()));
121
+ // We return account addresses based on the map keys that end with '-ivsk_m'
122
+ const accounts = allMapKeys.filter(key => key.endsWith('-ivsk_m')).map(key => key.split('-')[0]);
123
+ return accounts.map(account => AztecAddress.fromStringUnsafe(account));
124
+ }
125
+
126
+ /** Checks whether an account is registered in the key store. */
127
+ public async hasAccount(account: AztecAddress): Promise<boolean> {
128
+ return !!(await this.#db.transactionAsync(() => this.#keys.getAsync(`${account.toString()}-ivsk_m`)));
129
+ }
130
+
131
+ /**
132
+ * Gets the key validation request for a given master public key hash and contract address.
133
+ * @throws If the account corresponding to the master public key hash does not exist in the key store.
134
+ * @param pkMHash - The master public key hash.
135
+ * @param contractAddress - The contract address to silo the secret key in the key validation request with.
136
+ * @returns The key validation request.
137
+ */
138
+ public getKeyValidationRequest(pkMHash: Fr, contractAddress: AztecAddress): Promise<KeyValidationRequest> {
139
+ return this.#db.transactionAsync(async () => {
140
+ const [keyPrefix, account] = await this.getKeyPrefixAndAccount(pkMHash);
141
+
142
+ // Load the stored master public key point. The returned KVR carries only the hash, but we
143
+ // use the point here as a witness for two integrity checks below: (1) it matches the supplied
144
+ // hash, and (2) it matches the value derived from the stored secret key.
145
+ const pkMBuffer = await this.#keys.getAsync(`${account.toString()}-${keyPrefix}pk_m`);
146
+ if (!pkMBuffer) {
147
+ throw new Error(
148
+ `Could not find ${keyPrefix}pk_m for account ${account.toString()} whose address was successfully obtained with ${keyPrefix}pk_m_hash ${pkMHash.toString()}.`,
149
+ );
150
+ }
151
+
152
+ const pkM = Point.fromBuffer(pkMBuffer);
153
+
154
+ // Now we find the secret key for the public key
155
+ const skStorageSuffix = secretKeyStorageSuffix(keyPrefix);
156
+ const skMBuffer = await this.#keys.getAsync(`${account.toString()}-${skStorageSuffix}`);
157
+ if (!skMBuffer) {
158
+ throw new Error(
159
+ `Could not find ${skStorageSuffix} for account ${account.toString()} whose address was successfully obtained with ${keyPrefix}pk_m_hash ${pkMHash.toString()}.`,
160
+ );
161
+ }
162
+
163
+ const skM = GrumpkinScalar.fromBuffer(skMBuffer);
164
+
165
+ // The remaining awaits are non-DB computations. They are safe because no further IDB operations follow them.
166
+ const computedPkMHash = await hashPublicKey(pkM);
167
+ if (!computedPkMHash.equals(pkMHash)) {
168
+ throw new Error(`Could not find ${keyPrefix}pkM for ${keyPrefix}pk_m_hash ${pkMHash.toString()}.`);
169
+ }
170
+
171
+ const derivedPkM = await derivePublicKeyFromSecretKey(skM);
172
+ if (!derivedPkM.equals(pkM)) {
173
+ throw new Error(`Could not derive ${keyPrefix}pkM from ${keyPrefix}skM.`);
174
+ }
175
+
176
+ const skApp = await computeAppSecretKey(skM, contractAddress, keyPrefix!);
177
+
178
+ return new KeyValidationRequest(pkMHash, skApp);
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Gets the master nullifier public key for a given account.
184
+ * @throws If the account does not exist in the key store.
185
+ */
186
+ public async getMasterNullifierHidingPublicKey(account: AztecAddress): Promise<PublicKey> {
187
+ return Point.fromBuffer(await this.#getMasterKeyBuffer(account, 'npk_m'));
188
+ }
189
+
190
+ /**
191
+ * Gets the master incoming viewing public key for a given account.
192
+ * @throws If the account does not exist in the key store.
193
+ */
194
+ public async getMasterIncomingViewingPublicKey(account: AztecAddress): Promise<PublicKey> {
195
+ return Point.fromBuffer(await this.#getMasterKeyBuffer(account, 'ivpk_m'));
196
+ }
197
+
198
+ /**
199
+ * Retrieves the master outgoing viewing public key.
200
+ * @throws If the account does not exist in the key store.
201
+ */
202
+ public async getMasterOutgoingViewingPublicKey(account: AztecAddress): Promise<PublicKey> {
203
+ return Point.fromBuffer(await this.#getMasterKeyBuffer(account, 'ovpk_m'));
204
+ }
205
+
206
+ /**
207
+ * Retrieves the master tagging public key.
208
+ * @throws If the account does not exist in the key store.
209
+ */
210
+ public async getMasterTaggingPublicKey(account: AztecAddress): Promise<PublicKey> {
211
+ return Point.fromBuffer(await this.#getMasterKeyBuffer(account, 'tpk_m'));
212
+ }
213
+
214
+ /**
215
+ * Retrieves master incoming viewing secret key.
216
+ * @throws If the account does not exist in the key store.
217
+ */
218
+ public async getMasterIncomingViewingSecretKey(account: AztecAddress): Promise<GrumpkinScalar> {
219
+ return GrumpkinScalar.fromBuffer(await this.#getMasterKeyBuffer(account, 'ivsk_m'));
220
+ }
221
+
222
+ /**
223
+ * Retrieves the four privacy secret keys the key store holds for an account. Paired with {@link addAccount}, this
224
+ * allows exporting an account's privacy secret keys, e.g. to re-register it on another PXE. The message-signing and
225
+ * fallback secret keys are not held by the key store and so are not returned.
226
+ *
227
+ * @throws If the account does not exist in the key store.
228
+ */
229
+ public async getAccountSecretKeys(account: AztecAddress): Promise<AccountPrivacySecretKeys> {
230
+ const [nhkM, ivskM, ovskM, tskM] = await this.#getMasterKeyBuffers(account, ['nhk_m', 'ivsk_m', 'ovsk_m', 'tsk_m']);
231
+ return {
232
+ masterNullifierHidingSecretKey: GrumpkinScalar.fromBuffer(nhkM),
233
+ masterIncomingViewingSecretKey: GrumpkinScalar.fromBuffer(ivskM),
234
+ masterOutgoingViewingSecretKey: GrumpkinScalar.fromBuffer(ovskM),
235
+ masterTaggingSecretKey: GrumpkinScalar.fromBuffer(tskM),
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Retrieves application outgoing viewing secret key.
241
+ * @throws If the account does not exist in the key store.
242
+ * @param account - The account to retrieve the application outgoing viewing secret key for.
243
+ * @param app - The application address to retrieve the outgoing viewing secret key for.
244
+ * @returns A Promise that resolves to the application outgoing viewing secret key.
245
+ */
246
+ public async getAppOutgoingViewingSecretKey(account: AztecAddress, app: AztecAddress): Promise<Fr> {
247
+ const masterOutgoingViewingSecretKey = GrumpkinScalar.fromBuffer(await this.#getMasterKeyBuffer(account, 'ovsk_m'));
248
+
249
+ return poseidon2HashWithSeparator(
250
+ [masterOutgoingViewingSecretKey.hi, masterOutgoingViewingSecretKey.lo, app],
251
+ DomainSeparator.OVSK_M,
252
+ );
253
+ }
254
+
255
+ /**
256
+ * Retrieves the sk_m corresponding to the given pk_m hash.
257
+ * @throws If the provided hash is not associated with any of the registered accounts.
258
+ * @param pkMHash - The master public key hash to get secret key for.
259
+ * @returns A Promise that resolves to sk_m.
260
+ * @dev Used when feeding the sk_m to the kernel circuit for keys verification.
261
+ */
262
+ public getMasterSecretKey(pkMHash: Fr): Promise<GrumpkinScalar> {
263
+ return this.#db.transactionAsync(async () => {
264
+ const [keyPrefix, account] = await this.getKeyPrefixAndAccount(pkMHash);
265
+
266
+ const skStorageSuffix = secretKeyStorageSuffix(keyPrefix);
267
+ const secretKeyBuffer = await this.#keys.getAsync(`${account.toString()}-${skStorageSuffix}`);
268
+ if (!secretKeyBuffer) {
269
+ throw new Error(
270
+ `Could not find ${skStorageSuffix} for ${keyPrefix}pk_m_hash ${pkMHash.toString()}. This should not happen.`,
271
+ );
272
+ }
273
+
274
+ const skM = GrumpkinScalar.fromBuffer(secretKeyBuffer);
275
+
276
+ // Non-DB computation — safe because no further IDB operations follow.
277
+ // Integrity check: confirm the stored secret key still derives the requested hash. The check
278
+ // is hash-based rather than point-equal because the on-disk identifier is `pk_m_hash`;
279
+ // cryptographic collision resistance of `hashPublicKey` makes this equivalent to a
280
+ // direct point comparison in practice.
281
+ const derivedPkM = await derivePublicKeyFromSecretKey(skM);
282
+ const derivedPkMHash = await hashPublicKey(derivedPkM);
283
+ if (!derivedPkMHash.equals(pkMHash)) {
284
+ throw new Error(
285
+ `Could not find ${skStorageSuffix} for ${keyPrefix}pk_m_hash ${pkMHash.toString()} in secret keys buffer.`,
286
+ );
287
+ }
288
+
289
+ return skM;
290
+ });
291
+ }
292
+
293
+ /**
294
+ * Checks whether a given account has a key matching the provided master public key hash.
295
+ * @param account - The account address to check.
296
+ * @param pkMHash - The master public key hash to look for.
297
+ * @returns True if the account has a key with the given hash.
298
+ */
299
+ public accountHasKey(account: AztecAddress, pkMHash: Fr): Promise<boolean> {
300
+ return this.#db.transactionAsync(async () => {
301
+ const pkMHashBuffer = serializeToBuffer(pkMHash);
302
+ for (const prefix of KEY_PREFIXES) {
303
+ const stored = await this.#keys.getAsync(`${account.toString()}-${prefix}pk_m_hash`);
304
+ if (stored && Buffer.from(stored).equals(pkMHashBuffer)) {
305
+ return true;
306
+ }
307
+ }
308
+ return false;
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Gets the key prefix and account address for a given value.
314
+ * @returns A tuple containing the key prefix and account address.
315
+ * @dev Note that this is quite inefficient but it should not matter because there should never be too many keys
316
+ * in the key store.
317
+ */
318
+ public async getKeyPrefixAndAccount(value: Bufferable): Promise<[KeyPrefix, AztecAddress]> {
319
+ const valueBuffer = serializeToBuffer(value);
320
+ for await (const [key, val] of this.#keys.entriesAsync()) {
321
+ // Browser returns Uint8Array, Node.js returns Buffer
322
+ if (Buffer.from(val).equals(valueBuffer)) {
323
+ for (const prefix of KEY_PREFIXES) {
324
+ if (key.includes(`-${prefix}`)) {
325
+ const account = AztecAddress.fromStringUnsafe(key.split('-')[0]);
326
+ return [prefix, account];
327
+ }
328
+ }
329
+ }
330
+ }
331
+ throw new Error(`Could not find key prefix.`);
332
+ }
333
+
334
+ /**
335
+ * Persists a completed set of account keys and returns the resulting complete address.
336
+ */
337
+ async #storeAccountKeys(
338
+ accountKeys: Awaited<ReturnType<typeof completeAccountKeys>>,
339
+ partialAddress: PartialAddress,
340
+ ): Promise<CompleteAddress> {
341
+ const {
342
+ masterNullifierHidingSecretKey,
343
+ masterIncomingViewingSecretKey,
344
+ masterOutgoingViewingSecretKey,
345
+ masterTaggingSecretKey,
346
+ masterNullifierHidingPublicKey,
347
+ masterOutgoingViewingPublicKey,
348
+ masterTaggingPublicKey,
349
+ publicKeys,
350
+ } = accountKeys;
351
+
352
+ const completeAddress = await CompleteAddress.fromPublicKeysAndPartialAddress(publicKeys, partialAddress);
353
+ const { address: account } = completeAddress;
354
+
355
+ // completeAccountKeys has already guaranteed these master public keys are non-infinity, which the kernel cannot
356
+ // check but the address relies on.
357
+
358
+ // The npk/ovpk/tpk hashes are already in publicKeys; ivpk_m_hash is computed for indexing.
359
+ const masterIncomingViewingPublicKeyHash = await hashPublicKey(publicKeys.ivpkM);
360
+
361
+ await this.#db.transactionAsync(async () => {
362
+ // Naming of keys is as follows ${account}-${n/iv/ov/t}${sk/pk}_m.
363
+ //
364
+ // The message-signing and fallback keys are not stored: their secret keys are withheld from the key store, and
365
+ // their public keys are only needed transiently to compute the address (they live in the AddressStore).
366
+ await this.#keys.set(`${account.toString()}-ivsk_m`, masterIncomingViewingSecretKey.toBuffer());
367
+ await this.#keys.set(`${account.toString()}-ovsk_m`, masterOutgoingViewingSecretKey.toBuffer());
368
+ await this.#keys.set(`${account.toString()}-tsk_m`, masterTaggingSecretKey.toBuffer());
369
+ await this.#keys.set(`${account.toString()}-nhk_m`, masterNullifierHidingSecretKey.toBuffer());
370
+
371
+ await this.#keys.set(`${account.toString()}-npk_m`, masterNullifierHidingPublicKey.toBuffer());
372
+ await this.#keys.set(`${account.toString()}-ivpk_m`, publicKeys.ivpkM.toBuffer());
373
+ await this.#keys.set(`${account.toString()}-ovpk_m`, masterOutgoingViewingPublicKey.toBuffer());
374
+ await this.#keys.set(`${account.toString()}-tpk_m`, masterTaggingPublicKey.toBuffer());
375
+
376
+ // We store pk_m_hash under `account-{n/iv/ov/t}pk_m_hash` key to be able to obtain address and key prefix
377
+ // using the #getKeyPrefixAndAccount function later on
378
+ await this.#keys.set(`${account.toString()}-npk_m_hash`, publicKeys.npkMHash.toBuffer());
379
+ await this.#keys.set(`${account.toString()}-ivpk_m_hash`, masterIncomingViewingPublicKeyHash.toBuffer());
380
+ await this.#keys.set(`${account.toString()}-ovpk_m_hash`, publicKeys.ovpkMHash.toBuffer());
381
+ await this.#keys.set(`${account.toString()}-tpk_m_hash`, publicKeys.tpkMHash.toBuffer());
382
+ });
383
+
384
+ return completeAddress;
385
+ }
386
+
387
+ /**
388
+ * Fetches a stored master key buffer for an account by its storage suffix (e.g. `npk_m`, `ivsk_m`).
389
+ * @throws If the account does not exist in the key store.
390
+ */
391
+ async #getMasterKeyBuffer(account: AztecAddress, suffix: string): Promise<Buffer> {
392
+ const [buffer] = await this.#getMasterKeyBuffers(account, [suffix]);
393
+ return buffer;
394
+ }
395
+
396
+ /**
397
+ * Fetches multiple stored master key buffers for an account in a single transaction, returning them in the order of
398
+ * the requested storage suffixes.
399
+ * @throws If any of the keys is missing (i.e. the account does not exist in the key store).
400
+ */
401
+ async #getMasterKeyBuffers(account: AztecAddress, suffixes: string[]): Promise<Buffer[]> {
402
+ const buffers = await this.#db.transactionAsync(() =>
403
+ Promise.all(suffixes.map(suffix => this.#keys.getAsync(`${account.toString()}-${suffix}`))),
404
+ );
405
+ if (!buffers.every((buffer): buffer is Buffer => buffer !== undefined)) {
406
+ throw new Error(
407
+ `Account ${account.toString()} does not exist. Registered accounts: ${await this.getAccounts()}.`,
408
+ );
409
+ }
410
+ return buffers;
411
+ }
412
+ }