hardhat-keystore 插件全解析:为 Hardhat 配置变量打造加密存储
【免费下载链接】hardhatHardhat is a development environment to compile, deploy, test, and debug your Ethereum software.项目地址: https://gitcode.com/GitHub_Trending/ha/hardhat
本文以仓库内 packages/hardhat-keystore/README.md 为核心骨架,结合该包的完整源码、测试与错误定义进行纵深讲解。你将掌握:如何安装并启用
@nomicfoundation/hardhat-keystore插件;keystore系列 CLI 任务的完整用法与参数语义;生产 keystore 与开发 keystore 的差异及文件落盘位置;以及 scrypt 密钥派生、AES-GCM-SIV 数据加密、HMAC-SHA-256 完整性校验的底层加密原理。读完即可在自己的 Hardhat 项目(Hardhat 3.x,peerDependencies 为hardhat@^3.13.0)中安全地托管 API Key、私钥等敏感配置。
一、插件定位:把机密从配置文件和明文环境变量中解放出来
Hardhat 项目在配置中经常需要用到 API Key(如 Etherscan 验证密钥、Alchemy/Infura RPC 密钥)和私钥。传统做法要么直接写死在hardhat.config.ts里(一旦提交到 Git 就会泄露),要么依赖.env文件(仍是明文落盘)。@nomicfoundation/hardhat-keystore插件的目标就是提供一个加密的 keystore,用来在配置中安全地处理这些机密值。
包描述原文为:"A module for managing keystore files that store a map from IDs to encrypted string values."(见 packages/hardhat-keystore/package.json),即:一个管理 keystore 文件的模块,其中存储着"ID → 加密字符串值"的映射。机密值以加密形式保存在磁盘上,只有持有密码时才能解密读取。
与两个 Toolbox 的关系
该插件已内置于两个官方 Toolbox 中:
- Viem Hardhat Toolbox(对应仓库
packages/hardhat-toolbox-viem) - Ethers + Mocha Hardhat Toolbox(对应仓库
packages/hardhat-toolbox-mocha-ethers)
如果你已经在用其中任何一个 Toolbox,那么无需额外安装,插件已经随 Toolbox 一并引入。只有独立使用(不通过 Toolbox)时才需要手动安装。
安装与启用
npm install --save-dev @nomicfoundation/hardhat-keystore在hardhat.config.ts中导入插件并注册到plugins数组:
import { defineConfig } from "hardhat/config"; import hardhatKeystore from "@nomicfoundation/hardhat-keystore"; export default defineConfig({ plugins: [hardhatKeystore], });从插件入口 packages/hardhat-keystore/src/index.ts 可以看到,插件通过definePlugin注册了两类能力:
- hookHandlers:
config与configurationVariables两个 hook 处理器,分别负责向 Hardhat 配置注入 keystore 路径、以及接管配置变量的取值逻辑; - tasks:一组以
keystore为命名空间的 CLI 任务(set/get/list/delete/rename/path/change-password),外加一个空的父任务keystore(描述为 "Store keys in an encrypted storage")。
二、keystore 生命周期:从首次 set 到日常读写的完整流程
插件把机密的管理抽象成一条清晰的流水线(对应源码中的三个核心职责模块,见 packages/hardhat-keystore/src/internal/types.ts):
- KeystoreLoader:负责从磁盘加载/保存 keystore,校验磁盘文件结构,并通过内存缓存降低 IO 开销;
- FileManager:底层文件读写抽象(
fileExists/writeJsonFile/readJsonFile); - Keystore:内存中的 keystore 对象,封装
listUnverifiedKeys、hasKey、addNewValue、removeKey、readValue、isValidPassword等操作。
具体到文件加载器 packages/hardhat-keystore/src/internal/loaders/keystore-file-loader.ts,KeystoreFileLoader使用#keystoreCache缓存已加载的 Keystore 实例:
isKeystoreInitialized():先查缓存,缓存为空则检查 keystore 文件是否存在;loadKeystore():缓存命中直接返回,否则读取磁盘 JSON 文件并包装成Keystore实例;createUnsavedKeystore():仅当缓存为空(即 keystore 尚未初始化)时创建全新的空 keystore;saveKeystoreToFile():将内存中的EncryptedKeystore序列化写回磁盘。
首次初始化:keystore set的分支逻辑
以set任务(源码见 packages/hardhat-keystore/src/internal/tasks/set.ts)为例,首次使用与日常使用的路径截然不同:
const isKeystoreInitialized = await keystoreLoader.isKeystoreInitialized(); const password = isKeystoreInitialized ? await askPassword() : await setUpPassword(); if (isKeystoreInitialized === false) { await keystoreLoader.createUnsavedKeystore(createMasterKey({ password })); }- keystore 尚未初始化:进入"设置密码"流程(
setUpPassword),用密码派生 master key 并创建空 keystore; - keystore 已存在:直接询问密码(
askPassword),并用密码从已有 keystore 中重新派生 master key 解锁。
三、keystore命令族:七个任务的参数、语义与输出
所有任务均挂在keystore命名空间下,定义于 packages/hardhat-keystore/src/index.ts 的插件tasks数组中。绝大多数任务共享一个--dev标志,用于切换"开发 keystore"(详见第四节)。
1.npx hardhat keystore set <key>
设置(新增或覆盖)一个密钥,是初始化 keystore 的唯一入口(list/get等任务在 keystore 不存在时会提示先执行keystore set,对应displayNoKeystoreSetErrorMessage的消息:"No production keystore found. Please set one up using npx hardhat keystore set {key}")。
参数与行为(源自 src/index.ts 与 tasks/set.ts):
| 参数/标志 | 类型 | 说明 |
|---|---|---|
key | 位置参数(STRING) | 要存储的密钥名。必须以字母或下划线开头,仅允许字母、数字、下划线(正则^[a-zA-Z_]+[a-zA-Z0-9_]*$,见 validate-key.ts)。非法键名会打印红色错误并设置process.exitCode = 1 |
--force | 布尔标志 | 键已存在时强制覆盖;不加该标志,键已存在则报错退出 |
--dev | 布尔标志 | 使用开发 keystore 而非生产 keystore |
交互流程:如果 keystore 未初始化,先要求设置密码(密码需 ≥8 个字符,校验正则^.{8,}$,见 password.ts,且需二次输入确认,两次不一致会以红色提示重新输入);随后以交互方式输入要存储的机密值(空值会被拒绝:提示 "The value cannot be empty.");最后写入并落盘,输出 "Key " " set in the production keystore"。
2.npx hardhat keystore get <key>
按 key 读取并打印解密后的明文值。支持--dev。注意:命令行get任务会直接向终端输出机密值,适合调试;生产场景更推荐通过配置变量机制(第五节)读取,避免机密出现在 shell 历史或日志中。
3.npx hardhat keystore list
列出 keystore 中的所有 key(不显示值)。支持--dev。输出形如:
Keys in the production keystore: MY_API_KEY DEPLOYER_PRIVATE_KEY若 keystore 为空则输出 "The production keystore does not contain any keys."。实现上调用的是listUnverifiedKeys()——见 keystore.ts 中的注释,该方法不校验 keystore 完整性(返回的键名可能被篡改过),因为仅用于展示用途时风险可接受。
4.npx hardhat keystore delete <key>
删除指定 key。支持--dev与--force两个标志:--force的含义是"删除时键不存在也不抛错"(任务描述:"Force to not throw an error if the key does not exist during deletion")。删除成功输出 "Key " " deleted from the production keystore"。
5.npx hardhat keystore rename <oldKey> <newKey>
重命名一个 key,接收两个位置参数oldKey与newKey。支持--dev与--force,其中--force表示"新键名已存在时强制覆盖"。
6.npx hardhat keystore path
显示 keystore 文件的存储路径。支持--dev。用于快速确认当前使用的是哪个 keystore 文件。
7.npx hardhat keystore change-password
修改生产 keystore 的密码。仅支持生产 keystore——如果对开发 keystore 执行,会抛出错误码 50001CANNOT_CHANGED_PASSWORD_FOR_DEV_KEYSTORE(消息:"The keystore "change-password" task cannot be used with the development keystore",见 descriptors.ts)。流程要求先解锁当前密码,再输入新密码(同样要求 ≥8 字符并二次确认)。
四、生产 keystore 与开发 keystore:双轨隔离设计
这是本插件最具实用价值的设计之一。--dev标志背后的语义(源自 password.ts 与 get-keystore-file-path.ts):
| 维度 | 生产 keystore | 开发 keystore(--dev) |
|---|---|---|
| 存储文件 | 全局配置目录下keystore.json | 全局配置目录下dev.keystore.json |
| 密码来源 | 每次交互式输入 | 自动生成 16 字节随机 hex 串,明文写入hardhat.checksum文件 |
| 解锁方式 | 交互输入密码后经 scrypt 派生 | 直接读取密码文件 |
| 改密 | 支持change-password | 不允许(抛 50001 错误) |
| 适用场景 | 本地开发时保护真实密钥 | 测试、CI 中需要自动化运行且不想交互的场景 |
三个默认路径均由 config.ts 在resolveUserConfighook 中注入到hardhat.config的keystore字段(类型扩展见 type-extensions.ts):
declare module "hardhat/types/config" { export interface HardhatConfig { keystore: { filePath: string; devFilePath: string; devPasswordFilePath: string; }; } }对应的路径计算逻辑在 get-keystore-file-path.ts:
keystore.json:生产 keystore;dev.keystore.json:开发 keystore;hardhat.checksum:开发 keystore 的明文密码文件(文件名略出乎意料,但确实是源码中getDevKeystorePasswordFilePath返回的路径)。
setUpPasswordForDevKeystore用randomBytes(16).toString("hex")生成密码并写入该文件,而askPasswordForDevKeystore则直接readUtf8File读回——因此开发 keystore 完全无需人工交互,适合自动化与测试流水线。
五、配置变量接入:keystore 如何与 Hardhat 配置联动
这是把 keystore 用起来的关键环节。插件通过configurationVariableshook 的fetchValue处理器(源码见 configuration-variables.ts)接管了配置变量的取值逻辑,优先级设计为:
- 环境变量优先:
process.env[variable.name]已定义时,直接交给下一个处理器(环境变量 > keystore); - CI 中跳过 keystore:通过
isCi()检测,在 CI 环境不初始化也不读取 keystore(避免在 CI 中弹出密码交互),转交默认解析; - 先查开发 keystore:尝试从开发 keystore 取值;
- 测试模式约束:当
process.env.HH_TEST === "true"时,只允许使用开发 keystore,避免在测试中弹出生产 keystore 的密码输入——若开发 keystore 中没有该 key 且变量声明了默认值,则回退默认;否则抛出错误码 50002KEY_NOT_FOUND_DURING_TESTS_WITH_DEV_KEYSTORE(消息提示:"Key "{key}" not found in the development keystore. Run "npx hardhat keystore set {key} --dev" to set it."); - 最后查生产 keystore:开发 keystore 无此 key 且非测试场景时,读取生产 keystore(此步会触发密码交互)。
内部实现上有两处值得注意的优化:
- masterKey 缓存:
masterKeyProd/masterKeyDev在 hook 内缓存,多次取配置变量时不会反复弹密码框; - 未加锁检查:对带默认值的变量,会先通过
listUnverifiedKeys()检查明文的键名(不弹密码、不解锁 keystore),命中才解锁读取。代码注释明确指出这是一个权衡:跳过 HMAC 完整性校验,因此被篡改(key 被移除)的 keystore 会被当作"未命中"回退默认值,而不是报错。
官方使用指南(README 中链接的 configuration variables 指南)说明了配置侧的用法:在hardhat.config.ts中用variables声明配置变量(可带默认值),配合npx hardhat keystore set <KEY>存储机密,即可在配置中引用而不暴露明文。仓库内的 e2e 示例(e2e/fixture-projects/vars,含hardhat.config.js、package.json、test.sh)与packages/hardhat-keystore/test目录下的hook-handlers、tasks、keystores、loaders测试都是理解这一联动的可执行样例。
六、加密原理:scrypt + AES-GCM-SIV + HMAC-SHA-256 三层防护
本插件把密码学算法集中在单一文件 packages/hardhat-keystore/src/internal/keystores/encryption.ts 中,依赖@noble/ciphers与@noble/hashes(见 package.json)。文件结构(EncryptedKeystore接口)由version、crypto、dataEncryptionKey、hmacKey、hmac、secrets组成,其中所有二进制缓冲区都以无0x前缀的 hex 字符串表示。
6.1 密钥派生:scrypt
常量定义(源码第 11–33 行):
export const KEYSTORE_VERSION = "hardhat-v3-keystore-1"; export const PASSWORD_NORMALIZATION_FORM = "NFKC"; export const KEY_DERIVATION_ALGORITHM = "scrypt"; export const KEY_DERIVATION_PARAM_N = 131_072; // 2^17,内存约 128 MiB export const KEY_DERIVATION_PARAM_R = 8; export const KEY_DERIVATION_PARAM_P = 1; export const KEY_DERIVATION_SALT_LENGTH_BYTES = 32; export const MASTER_KEY_LENGTH_BITS = 256;参数N=2^17、r=8、p=1注释明确标注了来源:OWASP Password Storage Cheat Sheet 的 scrypt 建议参数,并引用了 noble-hashes 的 README。密码在派生前会先做NFKCUnicode 规范化(deriveMasterKey中password.normalize("NFKC")),避免不同输入法/系统下的等价字符产生不同的 key。每次创建 keystore 都会生成 32 字节随机 salt,salt 随 keystore 持久化,供后续从密码重新派生 master key(deriveMasterKeyFromKeystore)。
6.2 数据加密:AES-GCM-SIV(IV 碰撞容忍)
export const DATA_ENCRYPTION_ALGORITHM = "AES-GCM-SIV"; export const DATA_ENCRYPTION_KEY_LENGTH_BITS = 256; export const DATA_ENCRYPTION_IV_LENGTH_BYTES = 12;选型原因在源码注释中写明:AES-GCM-SIV 用于容忍 IV 碰撞(相比普通 GCM 在 IV 复用上的灾难性后果更稳健)。encryptUtf8String每次生成 12 字节随机 IV,注释同时给出了一个安全边界假设:随机 IV 只有 12 字节,因此假设同一密钥下加密次数不超过 2^20 次——此时 IV 碰撞概率才达到 2^-57 量级。
架构上采用双层密钥信封:master key(由密码经 scrypt 派生)不直接加密每个 secret,而是:
- 创建 keystore 时生成随机的
dataEncryptionKey(256 位)和hmacKey(256 位),分别用 master key 加密后存入dataEncryptionKey与hmacKey字段; - 每个 secret 使用
dataEncryptionKey加密(addSecretToKeystore/decryptSecret中先解出dataEncryptionKey再操作)。
这样即便某个 secret 被攻破,也不会直接暴露 master key 派生的口令信息。
6.3 完整性校验:HMAC-SHA-256 + 确定性 JSON 序列化
generateEncryptedKeystoreHmac用hmacKey对 keystore 的 JSON 序列化结果计算HMAC-SHA-256,而validateHmac在每次读/写前校验,防止密文被篡改。这里有个关键实现细节:deterministicJsonStringify——通过自定义JSON.stringifyreplacer 对对象键按字典序排序后再序列化,保证同一 keystore 在不同环境下产生完全一致的字节序列(数组、null 等不受支持的类型会被拒绝并抛出UnsupportedTypeInDeterministicJsonError)。HMAC 不匹配时抛出InvalidHmacError("Invalid hmac in keystore")。
6.4 错误体系
解密类错误被集中定义在该文件底部(不依赖 Hardhat 全局错误体系,保持模块自包含):
DecryptionError:解密失败,提示"确认使用了正确的密码/密钥且加密数据未被损坏";SecretNotFoundError:key 不存在于 keystore;HmacKeyDecryptionError:解密 hmac key 失败(通常意味着密码错误);InvalidHmacError:HMAC 校验失败(数据被篡改)。
而面向用户的错误则映射到@nomicfoundation/hardhat-errors的错误码体系(见 packages/hardhat-errors/src/descriptors.ts),错误码段50000–59999预留给hardhat-keystore:
| 错误码 | 名称 | 触发场景 |
|---|---|---|
| 50000 | INVALID_PASSWORD_OR_CORRUPTED_KEYSTORE | 密码错误或 keystore 文件损坏("Invalid password or corrupted keystore file.") |
| 50001 | CANNOT_CHANGED_PASSWORD_FOR_DEV_KEYSTORE | 对开发 keystore 执行 change-password |
| 50002 | KEY_NOT_FOUND_DURING_TESTS_WITH_DEV_KEYSTORE | 测试模式下开发 keystore 中找不到 key |
其中 50000 在Keystore类的每个方法(hasKey/readValue/removeKey/addNewValue/isValidPassword)中都有统一捕获转换逻辑——凡是捕获到HmacKeyDecryptionError就包装成 50000 抛给用户,见 keystore.ts。
七、Keystore 接口:类型层面的操作契约
最后从类型视角梳理Keystore接口(types.ts),它定义了所有 keystore 操作的方法签名,KeystoreFileLoader加载出的Keystore类(keystore.ts)即其实现:
export interface Keystore { listUnverifiedKeys(): Promise<string[]>; hasKey(key: string, masterKey: Uint8Array): Promise<boolean>; addNewValue(key: string, value: string, masterKey: Uint8Array): Promise<void>; removeKey(key: string, masterKey: Uint8Array): Promise<void>; readValue(key: string, masterKey: Uint8Array): Promise<string>; isValidPassword(masterKey: Uint8Array): Promise<void>; toJSON(): EncryptedKeystore; }注意所有方法都显式接收masterKey(Uint8Array)作为参数,且 master key 的注释强调"该值可以安全地留在内存中"(见deriveMasterKeyFromKeystore的 JSDoc),而解密出的 secret 则提醒"不要长期保存在内存中"(见decryptSecret的 JSDoc)——这是插件设计中对密钥生命周期管理的重要约定。
结语
@nomicfoundation/hardhat-keystore为 Hardhat 3.x 提供了一个"开箱即用、密码学严谨"的机密管理方案:CLI 层面用 7 个任务覆盖了 keystore 的完整生命周期,配置层面通过configurationVariableshook 与现有变量机制无缝集成,并通过--dev双轨设计兼顾了交互式开发与自动化测试两种场景。其底层选用 OWASP 推荐的 scrypt 参数、IV 碰撞容忍的 AES-GCM-SIV、以及 HMAC-SHA-256 完整性校验,配合确定性 JSON 序列化与双层密钥信封结构,构建了一套经得起推敲的加密存储实现。若要继续深入,建议直接阅读 encryption.ts(全部密码学核心)、tasks 目录(七个任务的实现)以及 test 目录(含fixture-projects的端到端测试样例)。
【免费下载链接】hardhatHardhat is a development environment to compile, deploy, test, and debug your Ethereum software.项目地址: https://gitcode.com/GitHub_Trending/ha/hardhat
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考