Eth Keystore
Overview
这是一个以太坊 keystore 文件的内容,我刚刚拿工具生成的:
{
"address": "0x7579e895f588d668c181710d74cc4c0ed4c8871b",
"crypto": {
"cipher": "aes-128-ctr",
"ciphertext": "d7e10d319e6a03bd6d4461da69c818418fc25f138bf98b854baf6fdd6feeeea5",
"cipherparams": {
"iv": "deb0e25a32ad2c8f28fa3d83c90c6a37"
},
"kdf": "scrypt",
"kdfparams": {
"dklen": 32,
"n": 262144,
"p": 1,
"r": 8,
"salt": "4595597c7002cfc47db91cbd1bf6faa9d9d4b45a205563537eb4e8bc33b0720b"
},
"mac": "2968ba58ac928feb6f95564e0083f186c4a605edfdffbbe35a7d47e0f32ed03c"
},
"id": "48258957-776e-40b7-9ebd-068dd8f1156a",
"version": 3
}
在了解每个字段含义之前,后面不会再解释的基础名词如下:
-
私钥(private key): 长度为
256 bit = 32 bytes
的随机的密钥,一切加密的源头 -
公钥(public key): 和私钥对应的公钥, 长度为
512 bit = 64 bytes
, 由 私钥 + 椭圆曲线加密 计算出来。可以由私钥推出公钥,无法从公钥推出私钥。具体的椭圆曲线算法参数secp256k1
和比特币一样。 -
账户地址(address): 由公钥进行
keccak256
hash 运算后取后20个字节,以太坊给账号作标记的地址。 -
密码(password): 用于加密
keystore
文件的密码,可以是任意长度的字符串。保护 keystore 文件不那么容易被破解。
再来解释上面 JSON 文件里每个字段的含义:
{
"address": "地址",
"crypto": {
"cipher": "使用的(对称加密的)加密方法名,默认使用对称 [AES 加密] 中的某一种",
"ciphertext": "私钥 + 密码 被该 cipher 加密方式加密后的结果",
"cipherparams": {
"iv": "该 cipher 加密方式需要的参数 (随机初始向量) `initialisation vector` "
},
"kdf": "使用的(基于密码的)加密方法名,`Key-derivation function` 一般用 [Scrypt] 也支持 [PBKDF2]",
"kdfparams": { // 该 kdf 加密方式需要的参数,除了盐值是随机生成的,其他参数推荐使用默认。
"dklen": 32, // 生成的 key 的 bytes 长度
"n": 262144, // 迭代次数,线性影响计算难度和内存占用
"p": 1, // 并行计算数
"r": 8, // 块大小,和 n 一起线性影响计算难度和内存占用
"salt": "盐值,随机生成的混淆参数"
},
"mac": "message authentication code 消息验证码,用于校验 password 是否正确"
},
"id": "随机生成的 UUID,和 keystore 加密本身关系不大",
"version": 3 // 版本号
}
整个 keystore 的加密,通过两类加密模式(cipher + kdf)来加密密码和私钥。从而达到的效果是:只有知道密码的用户才能利用 keystore 文件解出私钥,而想要暴力破解 keystore 也比较困难。
其中涉及到的加密算法 WIKI 链接:也许未来会进一步学习下啊巴啊巴…
Generate Keystore
来看看生成 keystore 的流程,目标是一个 keystore 文件和我们心中的密码,比如"some_password"
,它蕴含了一个32字节的私钥。私钥是生成 keystore 的数据源头,所以是整个算法过程中的参数。
除此之外,加上上面 JSON 文件里的众多参数里,可以分为三类:
随机的输入参数,由随机器/用户定义生成的内容
- 用户定义,不在文件里的私钥
- 用户定义,不在文件里的密码
-
随机生成:
cipherparams.iv
cipher 初始向量 -
随机生成:
kdfparams.salt
kdf 盐值
算法需要的参数或固定参数,控制了加密的强度或者其他标记
- kdfparams.dklen/n/p/r
- version
- id
算法的计算结果,由上面两类数据生成,写入文件为了校验/使用
- address: 对应了公钥
- ciphertext: 私钥加密结果
- mac: 验证码,用于在解密时验证密码是否正确
除掉相对固定的参数,整个方法可以认为是 \(F(prikey, password, iv, salt) \rightarrow mac, ciphertext\)
另外有 \(G(prikey) \rightarrow pubkey, address\) ,单独就可以生成。
Generate 算法步骤
私钥推出公钥和地址
\[G(prikey) \rightarrow pubkey, address\]
password + salt
经过 kdf 算法(Scrypt)
得到中间计算结果 key (256bits 32bytes)
\[Scrypt(password, salt, kdfparams) \rightarrow key\]
prikey
作为初始数据,经过对称加密 cipher 算法(AES128)
,应用数据:key
的前16字节,iv
的前16字节,得到加密后的 ciphertext
\[AES128(prikey, key[0..16], iv[0..16]) \rightarrow ciphertext\]
key[16..32] + ciphertext
合起来,经过 keccak256算法
作 hash256,得到结果 mac
\[keccak256(key[16..32] + ciphertext) \rightarrow mac\]
经过这四步,即把私钥和密码加密出来,计算出了校验用的 ciphertext
和 mac
Decrypt Keystore
解码 Keystore 需要 keystore 文件和密码,密码正确的结果是解出来的密钥,密码错误会出错。
Decrypt 算法步骤
根据输入的 password'
计算 key'
,此步骤算法同上面的 #2,因为输入和中间结果 key
不一定正确,后面加了一个 '
做区分
\[Scrypt(password', salt, kdfparams) \rightarrow key'\]
key'[16..32] + ciphertext
合起来,经过 keccak256算法
作 hash256,得到结果 mac'
, 和上面的步骤4一样,计算出 mac'
校验密码是否正确。
\[keccak256(key'[16..32] + ciphertext) \rightarrow mac'\]
验证 mac'
是否等于 mac
\[(mac'==mac)?continue:exit\]
ciphertext
作为初始数据,经过对称加密 cipher 算法(AES128)
,应用数据:key'
的前16字节,iv
的前16字节,恢复出私钥
\[AES128(ciphertext, key'[0..16], iv[0..16]) \rightarrow prikey\]
实现
加密和解密的方法,拿 Rust
实现了一下: 仓库链接
总结
完整的思维导图

推荐拓展阅读:
- https://julien-maffre.medium.com/what-is-an-ethereum-keystore-file-86c8c5917b97
- https://betterprogramming.pub/understanding-ethereum-cryptography-3ef7429eddce
- https://cryptobook.nakov.com/symmetric-key-ciphers/