在以太坊乃至整个加密货币世界中,私钥的安全性是资产安全的基石,用户通常不会直接与一长串无意义的十六进制私钥打交道,而是通过一个名为 Keystore 的文件来间接管理和保护它,这个文件通常以 UTC--<timestamp>_<address> 的命名方式出现,并常常伴随着一个密码,Keystore 的核心思想是:用用户自己设置的密码,对私钥进行加密,从而将私钥从“明文”转换为“密文”存储,即使文件泄露,没有密码也无法恢复出私钥。

本文将深入以太坊官方客户端 go-ethereum (geth) 的源码,剖析 Keystore 的实现原理,带你了解一个 Keystore 文件是如何被创建、解析以及其背后所依赖的加密学技术。

Keystore 文件是什么?

我们来看一个典型的 Keystore 文件内容(为了可读性,已格式化):

{
  "address": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
  "crypto": {
    "cipher": "aes-128-ctr",
    "ciphertext": "a1f8a7d4...",
    "cipherparams": {
      "iv": "6087dab2ef5d8fb1a6e8..."
    },
    "kdf": "scrypt",
    "kdfparams": {
      "dklen": 32,
      "n": 262144,
      "p": 1,
      "r": 8,
      "salt": "badae9f3e6..."
    },
    "mac": "7c1a9e1b..."
  },
  "id": "b8c8e5f0-3a2b-4e1d-9c6f-2a3b5c6d7e8f",
  "version": 3
}

从结构上可以看出,一个 Keystore 文件主要包含以下几个部分:

  1. address: 账户的以太坊地址,由私钥派生而来,这是公开信息。
  2. crypto: 加密相关的核心信息,包含了加密私钥所需的所有元数据。
    • cipher: 使用的对称加密算法,这里是 aes-128-ctr
    • ciphertext: 被加密后的私钥密文。
    • cipherparams: 对称加密算法的参数,主要是初始化向量 iv
    • kdf: 密钥派生函数,这里是 scrypt,用于从用户密码中派生出加密密钥。
    • kdfparams: KDF 的参数,如计算成本 n、并行度 p、内存成本 r 和盐值 salt,这些参数是抵抗暴力破解的关键。
    • mac: 消息认证码,用于验证解密过程是否成功,防止篡改。
  3. id: 一个随机生成的 UUID,用于唯一标识该 Keystore 文件。
  4. version: Keystore 的版本号,当前主流的是 3

理解了这个结构,我们就可以顺理成章地进入源码,看看这些字段是如何被生成的。

源码分析:Keystore 的创建与导出

go-ethereum 中,Keystore 的核心逻辑位于 accounts/keystore 包中,我们以导出 Keystore 为例,追踪其流程。

入口函数通常是 keystore.go 中的 Export 方法,但更核心的创建逻辑在 keystore.goencryptKey 函数中。

步骤 1:密码到加密密钥的派生

用户输入的密码本身并不直接用于加密私钥,直接使用密码作为密钥是不安全的,因为密码的熵通常较低,Keystore 使用一个密钥派生函数来“拉伸”密码,生成一个高强度的、长度合适的加密密钥。

// 位于 accounts/keystore/keystore.go
func (ks *keyStoreDir) encryptKey(key []byte, auth string) (cryptoJSON, error) {
    // ...
    // 1. 生成一个随机的盐值
    salt := crand.Reader crypto.GenerateRandomKey(32)
    // 2. 定义 scrypt 的参数,这些参数决定了 KDF 的计算强度
    // n: CPU/内存成本,r: 块大小,p: 并行度
    n := uint32(262144) // 2^18
    r := uint8(8)
    p := uint8(1)
    keyLen := uint32(32) // 派生出的密钥长度为 32 字节 (256-bit)
    // 3. 调用 scrypt KDF
    derivedKey, err := scrypt.Key([]byte(auth), salt, int(n), int(r), int(p), int(keyLen))
    // ...
}

源码解读:

  • salt (盐值):一个随机生成的 32 字节数组,它的作用是确保即使两个用户使用完全相同的密码,派生出的密钥也不同,从而有效抵御彩虹表攻击。
  • scrypt 函数:这是一个设计用来进行内存密集型计算的 KDF,它的参数 n, r, p 是其强度的核心。n 越大,需要的内存和 CPU 时间就越多,暴力破解的难度也就指数级增长。go-ethereum 选择了 n=262144 作为默认值,这是一个在安全性和性能之间取得良好平衡的配置。
  • derivedKey:这就是我们最终得到的加密密钥,它是一个 32 字节的数组,将被用于 AES 加密。

步骤 2:私钥的对称加密

有了派生出的密钥 derivedKey,接下来就是用它来加密真正的私钥 key,这里使用的是 aes-128-ctr (AES-128 in Counter mode) 算法。

// 位于 accounts/keystore/keystore.go (接上文)
func (ks *keyStoreDir) encryptKey(key []byte, auth string) (cryptoJSON, error) {
    // ... (derivedKey 已生成)
    // 4. 生成一个随机的初始化向量
    iv := crypto.GenerateRandomKey(16) // AES-128-CTR 需要 16 字节的 IV
    // 5. 准备 AES 加密器
    aesBlock, _ := aes.NewCipher(derivedKey[:16]) // 取 derivedKey 的前 16 字节作为 AES 密钥 (128-bit)
    stream := cipher.NewCTR(aesBlock, iv)
    // 6. 加密私钥
    ciphertext := make([]byte, len(key))
    stream.XORKeyStream(ciphertext, key)
    // ...
}

源码解读:

  • iv (初始化向量):同样是随机生成的 16 字节数组,CTR 模式将 IV 与计数器结合,生成一个密钥流,然后与明文进行异或操作得到密文,IV 必须是唯一的,但它不需要保密,可以和密文一起存储。
  • aes.NewCipher:创建一个 AES 加密块,这里只使用了 derivedKey 的前 16 个字节,因为 aes-128 需要 128 位(16 字节)的密钥。
  • cipher.NewCTR:创建一个 CTR 模式的加密流。
  • stream.XORKeyStream:这是 CTR 模式的核心操作,它直接对私钥进行流式加密,结果存入 ciphertext

步骤 3:生成消息认证码

为了确保 Keystore 文件的完整性和真实性,我们需要一个 MAC,MAC 的计算方式通常是:HMAC(derivedKey后半部分, ciphertext)

// 位于 accounts/keystore/keystore.go (接上文)
func (ks *keyStoreDir) encryptKey(key []byte, auth string) (cryptoJSON, error) {
    // ... (ciphertext 已生成)
    // 7. 计算 MAC
    // MAC 密钥是 derivedKey 的后 16 字节
    mac := crypto.NewMAC(derivedKey[16:])
    mac.Write(ciphertext)
    macSum := mac.Sum()
    // ...
}

源码解读:

  • derivedKey[16:]:我们使用派生密钥的后 16 个字节作为 HMAC 的密钥,这种设计将一个密钥“一分为二”,前半部分用于加密,后半部分用于认证,增加了安全性。
  • crypto.NewMACgo-ethereum 封装了一个 HMAC-SHA256 的实现。