@nomicfoundation/hardhat-keystore
Version:
A module for managing keystore files that store a map from IDs to encrypted string values.
125 lines (101 loc) • 3.96 kB
text/typescript
import type { KeystoreLoader } from "../types.js";
import type { ConfigurationVariable } from "hardhat/types/config";
import type {
ConfigurationVariableHooks,
HookContext,
} from "hardhat/types/hooks";
import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { isCi } from "@nomicfoundation/hardhat-utils/ci";
import { deriveMasterKeyFromKeystore } from "../keystores/encryption.js";
import { getPasswordHandlers } from "../keystores/password.js";
import { setupKeystoreLoaderFrom } from "../utils/setup-keystore-loader-from.js";
export default async (): Promise<Partial<ConfigurationVariableHooks>> => {
// Use a cache with hooks since they may be called multiple times consecutively.
let keystoreLoaderProd: KeystoreLoader | undefined;
let keystoreLoaderDev: KeystoreLoader | undefined;
// Caching the masterKey prevents repeated password prompts when retrieving multiple configuration variables.
let masterKeyProd: Uint8Array | undefined;
let masterKeyDev: Uint8Array | undefined;
const handlers: Partial<ConfigurationVariableHooks> = {
fetchValue: async (
context: HookContext,
variable: ConfigurationVariable,
next,
) => {
// Environment variables should take precedence over keystore
if (process.env[variable.name] !== undefined) {
return await next(context, variable);
}
// If we are in CI, the keystore should not be used
// or even initialized
if (isCi()) {
return await next(context, variable);
}
// First try to get the value from the development keystore
let value = await getValue(context, variable, true);
if (value !== undefined) {
return value;
}
if (process.env.HH_TEST === "true") {
// When `fetchValue` is called from a test, we only allow the use of the development keystore
// to avoid prompting for the production keystore password.
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_KEYSTORE.GENERAL.KEY_NOT_FOUND_DURING_TESTS_WITH_DEV_KEYSTORE,
{
key: variable.name,
},
);
}
// Then, if the development keystore does not have the key and `fetchValue` is not called from a test,
// attempt to retrieve the value from the production keystore.
value = await getValue(context, variable, false);
if (value !== undefined) {
return value;
}
return await next(context, variable);
},
};
async function getValue(
context: HookContext,
variable: ConfigurationVariable,
isDevKeystore: boolean,
): Promise<string | undefined> {
let keystoreLoader = isDevKeystore ? keystoreLoaderDev : keystoreLoaderProd;
let masterKey = isDevKeystore ? masterKeyDev : masterKeyProd;
if (keystoreLoader === undefined) {
keystoreLoader = setupKeystoreLoaderFrom(context, isDevKeystore);
if (isDevKeystore) {
keystoreLoaderDev = keystoreLoader;
} else {
keystoreLoaderProd = keystoreLoader;
}
}
if (!(await keystoreLoader.isKeystoreInitialized())) {
return undefined;
}
const keystore = await keystoreLoader.loadKeystore();
if (masterKey === undefined) {
const { askPassword } = getPasswordHandlers(
context.interruptions.requestSecretInput.bind(context.interruptions),
console.log,
isDevKeystore,
keystoreLoader.getKeystoreDevPasswordFilePath(),
);
const password = await askPassword();
masterKey = deriveMasterKeyFromKeystore({
encryptedKeystore: keystore.toJSON(),
password,
});
if (isDevKeystore) {
masterKeyDev = masterKey;
} else {
masterKeyProd = masterKey;
}
}
if (!(await keystore.hasKey(variable.name, masterKey))) {
return undefined;
}
return await keystore.readValue(variable.name, masterKey);
}
return handlers;
};