BIP39 密钥短语 这12个词实际上是什么,以及HD钱包派生的工作原理
一个BIP39助记词是将熵编码为单词——这是钱包恢复背后的密码学结构。本指南详细解释了单词列表的数学原理、PBKDF2种子步骤、BIP44派生路径、钱包为何对地址存在分歧,以及开发者需要了解的安全故障模式。
一个BIP39助记词是128位或256位的随机数据,使用一个固定的2048词列表编码成人类可读的单词。这就是全部的技巧。所谓的“魔法”只是单词比十六进制字符串更容易写在纸上——从密码学角度看,这些单词本身并没有特殊之处。
有趣的是,它建立在该编码之上的四层堆栈:BIP39(单词列表和编码)、BIP32(分层确定性密钥派生)、BIP43(目的字段约定)和BIP44(币/账户/地址结构)。大多数解释将这四层混为一谈。这个解释将它们分开。
单词列表:每个单词11位,包含校验码
这 BIP39英文单词列表 正好有2048个单词。211 = 2048,因此每个单词编码11位信息。一个12词短语总共携带132位信息;一个24词短语携带264位信息。
并非所有这些位都是熵。最后一个单词的最后几位是校验码——即SHA256(熵字节)的前ENT/32位,其中ENT是熵的长度(以位为单位):
| 短语长度 | 熵位(ENT) | 校验码位(CS = ENT/32) | 总位数 |
|---|---|---|---|
| 12个词 | 128 | 4 | 132 |
| 15个词 | 160 | 5 | 165 |
| 18个词 | 192 | 6 | 198 |
| 21个词 | 224 | 7 | 231 |
| 24个词 | 256 | 8 | 264 |
校验码是“几乎有效”助记词失败的原因。翻转一个熵位会使校验字节改变,从而使短语无效。这可以检测到输入错误——特别是那些落在校验码位上的错误。在派生密钥之前验证短语的钱包会完全拒绝该短语。有些钱包不进行验证,而是默默地从垃圾熵中派生一个钱包。
Python中的熵到单词映射:
import hashlib, os
# Generate 128 bits of entropy
entropy = os.urandom(16) # 16 bytes = 128 bits
# Compute checksum: first 4 bits of SHA256(entropy)
h = hashlib.sha256(entropy).digest()
checksum_bits = format(h[0], '08b')[:4] # first 4 bits of SHA256 output
# Combine entropy bits + checksum bits
all_bits = format(int.from_bytes(entropy, 'big'), '0128b') + checksum_bits
# all_bits is now 132 bits
# Split into 11-bit groups -> 12 word indices (0-2047)
word_indices = [int(all_bits[i:i+11], 2) for i in range(0, 132, 11)]
# Look up each index in the BIP39 word list to get the mnemonic
助记词不是密钥:PBKDF2步骤
这是大多数解释出错的地方。12个单词不是你的私钥,也不是直接用于签名。它们是中间编码。在任何密钥材料派生之前,助记词会通过PBKDF2-HMAC-SHA512进行扩展:
import hashlib
mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
passphrase = "" # optional; empty string = no passphrase
seed = hashlib.pbkdf2_hmac(
'sha512',
mnemonic.encode('utf-8'), # password: the mnemonic words
('mnemonic' + passphrase).encode('utf-8'), # salt: always "mnemonic" + passphrase
2048, # iterations
dklen=64 # 512 bits output
)
# seed is 64 bytes (512 bits) -- this is what actually seeds key derivation
需要注意的两点:
- 盐总是字面字符串
"mnemonic"与密码短语连接。空密码短语意味着盐就是"mnemonic"——没有单独的“无密码短语”模式。 - 密码短语会完全改变最终的种子。相同的12个单词加上密码短语
"A"vs"a"会产生完全不同的钱包和完全不同的地址。这就是“第25个词”功能:密码短语让你保持合理的否认能力(一个短语对应两个钱包),但一旦丢失密码短语,即使拥有12个单词也无法恢复。
从种子到主密钥:BIP32
512位种子通过HMAC-SHA512与固定密钥字符串 "Bitcoin seed"进行运算。64字节的输出分为两个256位部分:
import hmac, hashlib
master = hmac.new(b'Bitcoin seed', seed, hashlib.sha512).digest()
master_private_key = master[:32] # left 256 bits: the actual EC private key
master_chain_code = master[32:] # right 256 bits: used for all child derivation
链码是HD钱包能够工作的关键:它防止了即使已知父密钥,也能暴力破解派生密钥。如果没有链码,仅知道父公钥和任意子私钥,就会泄露所有兄弟私钥。有了链码,子密钥派生需要父密钥和链码作为输入——对于强化派生,还需要父私钥。
共同 master_private_key + master_chain_code 构成主扩展私钥,编码为一个 xprv... Base58Check字符串。相应的扩展公钥是 xpub... ——对需要派生地址但不暴露私钥材料的只读钱包非常有用。
BIP44派生路径:m/44’/60’/0’/0/0 解码
BIP44定义了一个五层派生层次结构。以下是 m/44’/60’/0’/0/0 ——标准以太坊第一个地址路径——逐层分解:
| 等级 | 价值 | 十六进制索引 | 意义 |
|---|---|---|---|
m | — | — | 主密钥根 |
44' | 目的 | 0x8000002C | BIP44目的。强化派生(撇号 = 0x80000000 + 索引)。 |
60' | 币类型 | 0x8000003C | 以太坊,根据 SLIP-0044。比特币 = 0′,Solana = 501′,等等。 |
0' | 账户 | 0x80000000 | 第一个账户。为独立账户递增。 |
0 | 变更 | 0 | 外部链(0 = 接收地址,1 = 变更地址)。钱包很少在以太坊链上使用变更链。 |
0 | 索引 | 0 | 地址索引。为第二个地址、第三个地址等递增。 |
撇号表示 强化派生。强化子密钥只能从父私钥派生,而不能从父公钥派生。这一点很重要,因为使用普通(非强化)派生时,如果一个子私钥被泄露,加上父公钥,就能暴露父私钥和所有兄弟密钥。在目的、币类型和账户层级上,强化派生是标准。
为什么相同的短语在不同钱包中会产生不同的地址
短语和钱包的默认路径是独立变量。钱包历史上对路径存在分歧,有些分歧至今仍未解决。
对于以太坊,主要分歧:
- MetaMask、Ledger Live、大多数现代钱包:
m/44'/60'/0'/0/N——标准BIP44。 - MyEtherWallet(旧版默认):
m/44'/60'/0'/N——少一个层级。从相同短语在MEW中使用旧路径恢复的钱包,其地址集与MetaMask完全不同。 - Trezor Suite: 现在遵循标准BIP44用于ETH,但历史上在一些替代币上存在自己的特殊规则。
对于比特币,分歧是结构性的:BIP44(m/44'/0'/0',旧式P2PKH,地址以1开头),BIP49(m/49'/0'/0',P2SH-P2WPKH,地址以3开头),和BIP84(m/84'/0'/0',原生segwit P2WPKH,地址以bc1q开头)都从相同的种子生成不同的地址。地址格式告诉你使用了哪条路径,这也是在排查恢复问题时地址格式重要的原因。
如果你导入一个短语后“资金不存在”,请先检查派生路径,再假设短语错误。大多数钱包在高级导入时允许你手动指定路径。
安全故障模式
这里的密码学并不是薄弱环节。所有针对BIP39钱包的攻击都针对人类环节。
钓鱼:输入您的恢复短语以继续
这是最高频的攻击,占显著比例。虚假钱包界面、浏览器扩展被恶意脚本注入、Discord中的假冒客服——都要求用户在某个地方输入他们的12个单词。正确的思维模型:合法的钱包软件在初始设置后永远不会要求输入种子短语。只要请求短语,就是攻击,无论界面看起来多可信。
剪贴板监控
恶意软件会轮询剪贴板,检测到12或24个已知BIP39单词序列,并进行窃取。窃取窗口仅为毫秒级。将种子短语复制到任何地方——即使只是短暂地粘贴到文本编辑器——都会造成暴露。剪贴板历史管理器(Windows剪贴板历史、macOS剪贴板管理器、IDE粘贴历史)尤其高风险。
截图和云照片同步
在手机上截图种子短语后,几秒钟内就会上传到iCloud照片或Google照片,大多数人还没来得及读完。这些备份在客户端未加密。‘我截图是为了安全保存’就是一条直接的泄露路径。将纸备份存放在物理安全位置,绝不是玩笑。
服务器端助记词生成
任何在服务器端生成BIP39短语的网站都拥有熵的副本。唯一安全的生成位置是在你控制的设备上,且在生成时是离线的。硬件钱包、空气隔离的机器或经过审计的客户端工具。网站不符合这些条件——即使JavaScript代码看起来正确,你也无法验证服务器没有记录输出。
如果你需要检查或验证短语的结构——检查熵、查看派生种子、验证路径——则 BIP39 助记符转换器 完全在浏览器中运行。短语永远不会离开你的机器,这是唯一安全的架构。
开发者角度:你几乎肯定不应该在你的应用中处理种子短语
如果你正在开发一个与加密相关的应用,将“在后端处理种子短语”的直觉几乎总是错误的。攻击面:
- 日志记录。 每个Web框架都会在某个地方记录请求体。一条调试信息、一个配置错误的日志级别,就会导致所有经过API的短语都保存在磁盘上——永久存在,分布在多个你未审计的日志目标中。
- 传输。 HTTPS保护了网络传输。它不保护负载均衡器、后端进程内存、数据库或日志聚合器。每个都是独立的漏洞面。
- 内存。 进程转储、崩溃报告、核心文件和堆快照会捕获内存中的字符串。一个种子短语在Python字典或JavaScript对象中并不等于零拷贝;它很可能在被“删除”前出现在多个分配中。
- 责任。 如果你的后端处理了用户种子短语,一旦被入侵,损害将是永久的。与密码不同,没有重置机制。受影响的用户会永久损失资金,且无任何救济途径。
真正有效的架构:在客户端派生所需内容,并仅传输输出——公钥、只读xpub或签名交易。后端永远不会看到短语。正确实现这一功能的库: @scure/bip39 (经过审计,依赖项最少), ethers.js,并且 bitcoinjs-lib。对于硬件钱包集成,Trezor和Ledger SDK返回签名交易——密钥永远不会离开设备。
完整的派生链概览
| 步 | 输入 | 手术 | 输出 |
|---|---|---|---|
| 1. 熵 | 128–256位随机位 | SHA256校验码 → 11位组 | 12–24词助记词 |
| 2. 种子 | 助记词单词 + “mnemonic” + 密码短语 | PBKDF2-HMAC-SHA516,2048轮 | 512位种子(64字节) |
| 3. 主密钥 | 种子字节 | HMAC-SHA512(“Bitcoin seed”, 种子) | 主私钥(256位)+ 链码(256位) |
| 4. 账户密钥 | 主密钥 + 路径 m/44’/60’/0’ | BIP32强化子密钥派生 × 3 | 账户扩展私钥 |
| 5. 地址密钥 | 账户密钥 + 路径 /0/N | BIP32普通子密钥派生 × 2 | 子私钥 → secp256k1公钥 → keccak256 → 地址 |
BIP39 正好做到了它设计的目的:让熵变得可写且可恢复。攻击面并不在密码学上——PBKDF2 2048轮、HMAC-SHA512、secp256k1 都是安全的。攻击完全是操作层面的:在不该的地方输入短语、将短语数字化存储、信任一个服务器端生成工具。数学是正确的。人类是薄弱环节,因此开发者建议是“设计系统,确保短语永远不会触碰你的基础设施。”
