Skip to content

Commit a7f9950

Browse files
authored
Merge pull request #7094 from NomicFoundation/dev-keystore
Add a development keystore
2 parents 4d08ea7 + f6f8d64 commit a7f9950

37 files changed

+2022
-1195
lines changed

.changeset/stupid-laws-carry.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@nomicfoundation/hardhat-errors": patch
3+
"@nomicfoundation/hardhat-keystore": patch
4+
"hardhat": patch
5+
---
6+
7+
Add a development keystore ([#7003](https://github.com/NomicFoundation/hardhat/issues/7003))

v-next/hardhat-errors/src/descriptors.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2048,6 +2048,20 @@ Please check Hardhat's output for more details.`,
20482048
websiteDescription:
20492049
"The password you provided is incorrect or the keystore file is corrupted.",
20502050
},
2051+
CANNOT_CHANGED_PASSWORD_FOR_DEV_KEYSTORE: {
2052+
number: 50001,
2053+
messageTemplate: `The keystore "change-password" task cannot be used with the development keystore`,
2054+
websiteTitle: "Cannot change password for dev keystore",
2055+
websiteDescription: `The keystore "change-password" task cannot be used with the development keystore`,
2056+
},
2057+
KEY_NOT_FOUND_DURING_TESTS_WITH_DEV_KEYSTORE: {
2058+
number: 50002,
2059+
messageTemplate: `Key "{key}" not found in the development keystore. Run "npx hardhat keystore set {key} --dev" to set it.`,
2060+
websiteTitle: "Key not found in the development keystore during tests",
2061+
websiteDescription: `Key not found in the development keystore. During tests, configuration variables can only be accessed through the development keystore.
2062+
2063+
Run "npx hardhat keystore set <KEY> --dev" to set it.`,
2064+
},
20512065
},
20522066
},
20532067
NETWORK_HELPERS: {

v-next/hardhat-keystore/src/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,20 @@ const hardhatKeystorePlugin: HardhatPlugin = {
3131
name: "force",
3232
description: "Forces overwrite if the key already exists.",
3333
})
34+
.addFlag({
35+
name: "dev",
36+
description:
37+
"Use the development keystore instead of the production one",
38+
})
3439
.setAction(import.meta.resolve("./internal/tasks/set.js"))
3540
.build(),
3641

3742
task(["keystore", "get"], "Get a value given a key")
43+
.addFlag({
44+
name: "dev",
45+
description:
46+
"Use the development keystore instead of the production one",
47+
})
3848
.addPositionalArgument({
3949
name: "key",
4050
type: ArgumentType.STRING,
@@ -44,10 +54,20 @@ const hardhatKeystorePlugin: HardhatPlugin = {
4454
.build(),
4555

4656
task(["keystore", "list"], "List all keys in the keystore")
57+
.addFlag({
58+
name: "dev",
59+
description:
60+
"Use the development keystore instead of the production one",
61+
})
4762
.setAction(import.meta.resolve("./internal/tasks/list.js"))
4863
.build(),
4964

5065
task(["keystore", "delete"], "Delete a key from the keystore")
66+
.addFlag({
67+
name: "dev",
68+
description:
69+
"Use the development keystore instead of the production one",
70+
})
5171
.addPositionalArgument({
5272
name: "key",
5373
type: ArgumentType.STRING,
@@ -62,13 +82,23 @@ const hardhatKeystorePlugin: HardhatPlugin = {
6282
.build(),
6383

6484
task(["keystore", "path"], "Display the path where the keystore is stored")
85+
.addFlag({
86+
name: "dev",
87+
description:
88+
"Use the development keystore instead of the production one",
89+
})
6590
.setAction(import.meta.resolve("./internal/tasks/path.js"))
6691
.build(),
6792

6893
task(
6994
["keystore", "change-password"],
7095
"Change the password for the keystore",
7196
)
97+
.addFlag({
98+
name: "dev",
99+
description:
100+
"Use the development keystore instead of the production one",
101+
})
72102
.setAction(import.meta.resolve("./internal/tasks/change-password.js"))
73103
.build(),
74104
],

v-next/hardhat-keystore/src/internal/hook-handlers/config.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import type { ConfigHooks } from "hardhat/types/hooks";
22

33
import debug from "debug";
44

5-
import { getKeystoreFilePath } from "../utils/get-keystore-file-path.js";
5+
import {
6+
getDevKeystoreFilePath,
7+
getDevKeystorePasswordFilePath,
8+
getKeystoreFilePath,
9+
} from "../utils/get-keystore-file-path.js";
610

711
const log = debug("hardhat:keystore:hooks:config");
812

@@ -19,13 +23,18 @@ export default async (): Promise<Partial<ConfigHooks>> => {
1923
);
2024

2125
const defaultKeystoreFilePath = await getKeystoreFilePath();
26+
const defaultDevKeystoreFilePath = await getDevKeystoreFilePath();
27+
const defaultDevKeystorePasswordFilePath =
28+
await getDevKeystorePasswordFilePath();
2229

2330
log(`path to keystore file: ${defaultKeystoreFilePath}`);
2431

2532
return {
2633
...resolvedConfig,
2734
keystore: {
2835
filePath: defaultKeystoreFilePath,
36+
devFilePath: defaultDevKeystoreFilePath,
37+
devPasswordFilePath: defaultDevKeystorePasswordFilePath,
2938
},
3039
};
3140
},

v-next/hardhat-keystore/src/internal/hook-handlers/configuration-variables.ts

Lines changed: 81 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@ import type {
55
HookContext,
66
} from "hardhat/types/hooks";
77

8+
import { HardhatError } from "@nomicfoundation/hardhat-errors";
89
import { isCi } from "@nomicfoundation/hardhat-utils/ci";
910

1011
import { deriveMasterKeyFromKeystore } from "../keystores/encryption.js";
11-
import { askPassword } from "../keystores/password.js";
12+
import { getPasswordHandlers } from "../keystores/password.js";
1213
import { setupKeystoreLoaderFrom } from "../utils/setup-keystore-loader-from.js";
1314

1415
export default async (): Promise<Partial<ConfigurationVariableHooks>> => {
1516
// Use a cache with hooks since they may be called multiple times consecutively.
16-
let keystoreLoader: KeystoreLoader | undefined;
17+
let keystoreLoaderProd: KeystoreLoader | undefined;
18+
let keystoreLoaderDev: KeystoreLoader | undefined;
1719
// Caching the masterKey prevents repeated password prompts when retrieving multiple configuration variables.
18-
let masterKey: Uint8Array | undefined;
20+
let masterKeyProd: Uint8Array | undefined;
21+
let masterKeyDev: Uint8Array | undefined;
1922

2023
const handlers: Partial<ConfigurationVariableHooks> = {
2124
fetchValue: async (
@@ -29,34 +32,91 @@ export default async (): Promise<Partial<ConfigurationVariableHooks>> => {
2932
return next(context, variable);
3033
}
3134

32-
if (keystoreLoader === undefined) {
33-
keystoreLoader = setupKeystoreLoaderFrom(context);
35+
// When `fetchValue` is called from a test, we only allow the use of the development keystore
36+
// to avoid prompting for the production keystore password.
37+
const onlyAllowDevKeystore = process.env.HH_TEST === "true";
38+
39+
// First try to get the value from the development keystore
40+
let value = await getValue(context, variable, true, onlyAllowDevKeystore);
41+
42+
if (value !== undefined) {
43+
return value;
3444
}
3545

36-
if (!(await keystoreLoader.isKeystoreInitialized())) {
37-
return next(context, variable);
46+
// Then, if the development keystore does not have the key and `fetchValue` is not called from a test,
47+
// attempt to retrieve the value from the production keystore.
48+
value = await getValue(context, variable, false, onlyAllowDevKeystore);
49+
50+
if (value !== undefined) {
51+
return value;
3852
}
3953

40-
const keystore = await keystoreLoader.loadKeystore();
54+
return next(context, variable);
55+
},
56+
};
57+
58+
async function getValue(
59+
context: HookContext,
60+
variable: ConfigurationVariable,
61+
isDevKeystore: boolean,
62+
onlyAllowDevKeystore: boolean,
63+
): Promise<string | undefined> {
64+
let keystoreLoader = isDevKeystore ? keystoreLoaderDev : keystoreLoaderProd;
65+
let masterKey = isDevKeystore ? masterKeyDev : masterKeyProd;
4166

42-
if (masterKey === undefined) {
43-
const password = await askPassword(
44-
context.interruptions.requestSecretInput.bind(context.interruptions),
45-
);
67+
if (keystoreLoader === undefined) {
68+
keystoreLoader = setupKeystoreLoaderFrom(context, isDevKeystore);
4669

47-
masterKey = deriveMasterKeyFromKeystore({
48-
encryptedKeystore: keystore.toJSON(),
49-
password,
50-
});
70+
if (isDevKeystore) {
71+
keystoreLoaderDev = keystoreLoader;
72+
} else {
73+
keystoreLoaderProd = keystoreLoader;
5174
}
75+
}
5276

53-
if (!(await keystore.hasKey(variable.name, masterKey))) {
54-
return next(context, variable);
77+
if (!(await keystoreLoader.isKeystoreInitialized())) {
78+
return undefined;
79+
}
80+
81+
const keystore = await keystoreLoader.loadKeystore();
82+
83+
if (masterKey === undefined) {
84+
const { askPassword } = getPasswordHandlers(
85+
context.interruptions.requestSecretInput.bind(context.interruptions),
86+
console.log,
87+
isDevKeystore,
88+
keystoreLoader.getKeystoreDevPasswordFilePath(),
89+
);
90+
91+
const password = await askPassword();
92+
93+
masterKey = deriveMasterKeyFromKeystore({
94+
encryptedKeystore: keystore.toJSON(),
95+
password,
96+
});
97+
98+
if (isDevKeystore) {
99+
masterKeyDev = masterKey;
100+
} else {
101+
masterKeyProd = masterKey;
55102
}
103+
}
56104

57-
return keystore.readValue(variable.name, masterKey);
58-
},
59-
};
105+
if (!(await keystore.hasKey(variable.name, masterKey))) {
106+
if (onlyAllowDevKeystore) {
107+
throw new HardhatError(
108+
HardhatError.ERRORS.HARDHAT_KEYSTORE.GENERAL.KEY_NOT_FOUND_DURING_TESTS_WITH_DEV_KEYSTORE,
109+
{
110+
key: variable.name,
111+
},
112+
);
113+
}
114+
115+
return undefined;
116+
}
117+
118+
return keystore.readValue(variable.name, masterKey);
119+
}
60120

61121
return handlers;
62122
};

v-next/hardhat-keystore/src/internal/keystores/password.ts

Lines changed: 61 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,49 @@
1+
import type {
2+
KeystoreConsoleLog,
3+
KeystoreRequestSecretInput,
4+
} from "../types.js";
5+
6+
import { randomBytes } from "node:crypto";
7+
8+
import { HardhatError } from "@nomicfoundation/hardhat-errors";
9+
import { readUtf8File, writeUtf8File } from "@nomicfoundation/hardhat-utils/fs";
110
import chalk from "chalk";
211

312
import { PLUGIN_ID } from "../constants.js";
413
import { UserDisplayMessages } from "../ui/user-display-messages.js";
514

15+
export function getPasswordHandlers(
16+
requestSecretInput: KeystoreRequestSecretInput,
17+
consoleLog: KeystoreConsoleLog,
18+
isDevKeystore: boolean,
19+
devPasswordFilePath: string,
20+
): {
21+
setUpPassword: () => Promise<string>;
22+
askPassword: () => Promise<string>;
23+
setNewPassword: (password: string) => Promise<string>;
24+
} {
25+
if (isDevKeystore) {
26+
return {
27+
setUpPassword: () => setUpPasswordForDevKeystore(devPasswordFilePath),
28+
askPassword: () => askPasswordForDevKeystore(devPasswordFilePath),
29+
setNewPassword: () => {
30+
throw new HardhatError(
31+
HardhatError.ERRORS.HARDHAT_KEYSTORE.GENERAL.CANNOT_CHANGED_PASSWORD_FOR_DEV_KEYSTORE,
32+
);
33+
},
34+
};
35+
}
36+
37+
return {
38+
setUpPassword: () => setUpPassword(requestSecretInput, consoleLog),
39+
askPassword: () => askPassword(requestSecretInput),
40+
setNewPassword: () => setNewPassword(requestSecretInput, consoleLog),
41+
};
42+
}
43+
644
export async function setUpPassword(
7-
requestSecretInput: (
8-
interruptor: string,
9-
inputDescription: string,
10-
) => Promise<string>,
11-
consoleLog: (text: string) => void = console.log,
45+
requestSecretInput: KeystoreRequestSecretInput,
46+
consoleLog: KeystoreConsoleLog = console.log,
1247
): Promise<string> {
1348
consoleLog(UserDisplayMessages.keystoreBannerMessage());
1449

@@ -19,12 +54,19 @@ export async function setUpPassword(
1954
return createPassword(requestSecretInput, consoleLog);
2055
}
2156

57+
export async function setUpPasswordForDevKeystore(
58+
devPasswordFilePath: string,
59+
): Promise<string> {
60+
const password = randomBytes(16).toString("hex");
61+
62+
await writeUtf8File(devPasswordFilePath, password);
63+
64+
return password;
65+
}
66+
2267
export async function setNewPassword(
23-
requestSecretInput: (
24-
interruptor: string,
25-
inputDescription: string,
26-
) => Promise<string>,
27-
consoleLog: (text: string) => void = console.log,
68+
requestSecretInput: KeystoreRequestSecretInput,
69+
consoleLog: KeystoreConsoleLog = console.log,
2870
): Promise<string> {
2971
consoleLog(UserDisplayMessages.passwordChangeMessage());
3072
consoleLog(UserDisplayMessages.passwordRequirementsMessage());
@@ -34,20 +76,20 @@ export async function setNewPassword(
3476
}
3577

3678
export async function askPassword(
37-
requestSecretInput: (
38-
interruptor: string,
39-
inputDescription: string,
40-
) => Promise<string>,
79+
requestSecretInput: KeystoreRequestSecretInput,
4180
): Promise<string> {
4281
return requestSecretInput(PLUGIN_ID, UserDisplayMessages.enterPasswordMsg());
4382
}
4483

84+
export async function askPasswordForDevKeystore(
85+
devPasswordFilePath: string,
86+
): Promise<string> {
87+
return readUtf8File(devPasswordFilePath);
88+
}
89+
4590
async function createPassword(
46-
requestSecretInput: (
47-
interruptor: string,
48-
inputDescription: string,
49-
) => Promise<string>,
50-
consoleLog: (text: string) => void = console.log,
91+
requestSecretInput: KeystoreRequestSecretInput,
92+
consoleLog: KeystoreConsoleLog = console.log,
5193
) {
5294
const PASSWORD_REGEX = /^.{8,}$/;
5395

0 commit comments

Comments
 (0)