使用 KDF 生成秘钥
密码学KDF(key derivation functions),其作用是通过一个密码派生出一个或多个秘钥,即从 password 生成加密用的 key。
而在Keystore中,是用的是Scrypt算法,用一个公式来表示的话,派生的Key生成方程为:
DK = Scrypt(salt, dk_len, n, r, p)
其中的 salt 是一段随机的盐,dk_len 是输出的哈希值的长度。n 是 CPU/Memory 开销值,越高的开销值,计算就越困难。r 表示块大小,p 表示并行度。
Litecoin 就使用 scrypt 作为它的 POW 算法
对私钥进行对称加密
上面已经用KDF算法生成了一个秘钥,这个秘钥就是接着进行对称加密的秘钥,这里使用的对称加密算法是 aes-128-ctr,aes-128-ctr 加密算法还需要用到一个参数初始化向量 iv。
Keystore文件
{
"address":"856e604698f79cef417aab...",
"crypto":{
"cipher":"aes-128-ctr",
"ciphertext":"13a3ad2135bef1ff228e399dfc8d7757eb4bb1a81d1b31....",
"cipherparams":{
"iv":"92e7468e8625653f85322fb3c..."
},
"kdf":"scrypt",
"kdfparams":{
"dklen":32,
"n":262144,
"p":1,
"r":8,
"salt":"3ca198ce53513ce01bd651aee54b16b6a...."
},
"mac":"10423d837830594c18a91097d09b7f2316..."
},
"id":"5346bac5-0a6f-4ac6-baba-e2f3ad464f3f",
"version":3
}
来解读一下各个字段:
在Keystore文件中,有以下重要字段:
- address: 这是账号的地址。
- version: Keystore文件的版本,目前为第3版,也称为V3 KeyStore。
- id: 这是一个唯一标识符(UUID),用于标记Keystore文件的唯一性。
- crypto: 这部分包括了与加密相关的配置信息。
- cipher: 它是用于加密以太坊私钥的对称加密算法,具体使用的是aes-128-ctr算法。
- cipherparams: 这包括了aes-128-ctr加密算法所需的参数,主要是初始化向量(IV)。
- ciphertext: 这是加密算法输出的密文,也是在将来解密时所需的输入。
- kdf: 该字段指定了使用的密钥派生函数(Key Derivation Function)算法,这里使用的是scrypt。
- kdfparams: 这部分包括了scrypt函数所需的参数。
- mac: 用来验证密码正确性的消息认证码(MAC),计算方式是mac = sha3(DK[16:32], ciphertext)。
这些字段共同构成了Keystore文件,确保了私钥的安全存储和保护。
总结一下Keystore 文件的产生:
- 使用scrypt函数 (根据密码 和 相应的参数) 生成秘钥
- 使用上一步生成的秘钥 + 账号私钥 + 参数 进行对称加密。
- 把相关的参数 和 输出的密文 保存为以上格式的 JSON 文件
如何确保密码是对的?
当我们在使用Keystore文件来还原私钥时,依然是使用kdf生成一个秘钥,然后用秘钥对ciphertext进行解密
正是在这一步,不论你使用什么密码,都将生成一个私钥。然而,最终生成的以太坊私钥的正确性却仍然是一个谜。
这就是Keystore文件中MAC(消息认证码)值的作用。MAC值是通过对KDF输出和密文进行SHA3-256哈希运算得到的结果。显然,不同的密码会产生不同的MAC值,因此MAC值用于验证密码的正确性。验证过程可以用下图表示:

用ethers.js 实现账号导出导入
ethers.js 直接提供了加载keystore JSON来创建钱包对象以及加密生成keystore文件的方法,方法如下:
// 导入keystore Json
ethers.Wallet.fromEncryptedJson(json, password, [progressCallback]).then(function(wallet) {
// wallet
});
// 使用钱包对象 导出keystore Json
wallet.encrypt(pwd, [progressCallback].then(function(json) {
// 保存json
});
现在结合界面来完整的实现账号导出及导入,UI图如下:
HTML 代码如下:
<h3>KeyStore 导出:</h3>
<table>
<tr>
<th>密码:</th>
<td><input type="text" placeholder="(password)" id="save-keystore-file-pwd" /></td>
</tr>
<tr>
<td> </td>
<td>
<div id="save-keystore" class="submit">导出</div>
</td>
</tr>
</table>
上面主要定义了一个密码输入框和一个导出按钮,点击“导出”后,处理逻辑代码如下:
// "导出" 按钮,执行exportKeystore函数
$('#save-keystore').click(exportKeystore);
exportKeystore: function() {
// 获取密码
var pwd = $('#save-keystore-file-pwd');
// wallet 是上一篇文章中生成的钱包对象
wallet.encrypt(pwd.val()).then(function(json) {
var blob = new Blob([json], {type: "text/plain;charset=utf-8"});
// 使用了FileSaver.js 进行文件保存
saveAs(blob, "keystore.json");
});
}
FileSaver.js 是可以用来在页面保存文件的一个库。
再来看看导入keystore 文件, UI图如下:
<h2>加载账号Keystore文件</h2>
<table>
<tr>
<th>Keystore:</th>
<td><div class="file" id="select-wallet-drop">把Json文件拖动到这里</div><input type="file" id="select-wallet-file" /></td>
</tr>
<tr>
<th>密码:</th>
<td><input type="password" placeholder="(password)" id="select-wallet-password" /></td>
</tr>
<tr>
<td> </td>
<td>
<div id="select-submit-wallet" class="submit disable">解密</div>
</td>
</tr>
</table>
上述代码片段主要包含一个文件输入框、一个密码输入框和一个“解密”按钮。因此,它的主要逻辑分为两部分:一是文件读取,二是解析和加载账户。以下是关键代码:
// 使用FileReader读取文件,
var fileReader = new FileReader();
fileReader.onload = function(e) {
var json = e.target.result;
// 从加载
ethers.Wallet.fromEncryptedJson(json, password).then(function(wallet) {
}, function(error) {
});
};
fileReader.readAsText(inputFile.files[0]);
这部分就介绍到这里
开发联系:DEXDAO