2.5 节完成了数字签名的数学实现。本节转入工程实践——如何将密码学安全私钥安全地生成、存储和使用,这是用户与区块链交互的第一入口。钱包不是"存放币的地方",而是密钥的管理器。
2.6.1 密钥生成的起点:密码学安全随机数
为什么需要 CSPRNG?
普通伪随机数生成器(如 Math.random())是可预测的。如果你用 Math.random() 生成私钥,攻击者可以通过观察少量输出推断出整个序列,从而推算出你的私钥。
密码学安全随机数生成器(CSPRNG) 的要求:
- 前向不可预测性:即使攻击者知道前 个输出,也无法预测第 个。
- 后向不可预测性:即使攻击者知道后续输出,也无法推算出之前的内部状态(防止逆向推导种子)。
/**
* 密码学安全随机数生成(基于浏览器/Node crypto API)
*/
function secureRandomBytes(size: number): Uint8Array {
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
// 浏览器环境
return crypto.getRandomValues(new Uint8Array(size));
} else if (typeof require === 'function') {
// Node.js 环境
const nodeCrypto = require('crypto');
return nodeCrypto.randomBytes(size);
}
throw new Error('No CSPRNG available');
}
// 生成一个 secp256k1 私钥:256 位随机数,需满足 1 <= d < n
function generatePrivateKey(): bigint {
const n = SECP256K1.n;
while (true) {
const bytes = secureRandomBytes(32);
let d = 0n;
for (let i = 0; i < 32; i++) {
d = (d << 8n) | BigInt(bytes[i]);
}
if (d >= 1n && d < n) return d;
}
}
console.log("=== 安全私钥生成 ===");
const d = generatePrivateKey();
console.log(`私钥: 0x${d.toString(16).padStart(64, '0')}`);
console.log(`位数: ${d.toString(2).length}`);
console.log(`有效范围: ${d > 0n && d < SECP256K1.n ? '✅ 有效' : '❌ 无效'}`);熵的物理来源
crypto.randomBytes 的核心是操作系统内核收集的真实熵源:
- 硬件中断时间(键盘按键、磁盘 I/O、网络包到达时间)
- CPU 硬件随机数生成器(如 Intel RDRAND 指令)
- 其他不可预测的硬件事件
关键规则:永远不要自己写随机数生成器。"密码学家唯一会自己实现的随机数生成器,就是另一个会被攻破的随机数生成器。"
2.6.2 BIP-39:从随机熵到人类可记忆的助记词
flowchart LR
RNG[密码学安全随机数<br/>熵 128-256 bit] --> MSE[助记词<br/>12-24 个英文单词]
MSE --> Seed[种子<br/>PBKDF2-HMAC-SHA512<br/>2000 轮迭代]
Seed --> XK[主扩展密钥<br/>BIP-32]
XK --> Chain[子密钥派生<br/>CKD: 父密钥 + 索引]
Chain --> Tree[HD 派生树<br/>BIP-44: m/44'/0'/0'/i]
Tree --> Addr[比特币/以太坊地址]
style MSE fill:#fff3e0
style XK fill:#e3f2fd
style Addr fill:#c8e6c9
为什么需要助记词?
256 位的十六进制字符串(64 个字符)对人类极不友好。抄错一个字符 = 资产永久丢失。BIP-39 将 128–256 位的熵映射为 12–24 个英文单词(从一个 2048 词的固定词表中选取),大幅降低抄写错误率。
BIP-39 的完整流程
128-256 位随机熵
│
▼
┌─────────────────┐
│ SHA-256 哈希 │ → 取前 (熵位数/32) 位作为"校验和"
└─────────────────┘
│
▼
熵 + 校验和 = 264-330 位
│
▼ 每 11 位 = 一个单词索引(0-2047)
12-24 个助记词/**
* BIP-39 助记词简化实现
* 使用 128 位熵 = 12 词 + 4 位校验和
*/
const BIP39_WORD_LIST: string[] = [
"abandon", "ability", "able", // ... 完整词表有 2048 个词
// 教学演示:使用前 64 个词做简化版
...Array.from({ length: 2048 }, (_, i) => `word${i.toString(16).padStart(3, '0')}`) // 占位符
];
// 真实词表应使用标准英文词表(如 bitcoin/bips 仓库中的 bip-0039/english.txt)
class BIP39 {
private wordList: string[];
constructor(wordList: string[] = BIP39_WORD_LIST) {
if (wordList.length !== 2048) throw new Error('Word list must have 2048 entries');
this.wordList = wordList;
}
/**
* 熵 → 助记词
* @param entropy 128, 160, 192, 224, 或 256 位的 Uint8Array
*/
entropyToMnemonic(entropy: Uint8Array): string {
const ENT = entropy.length * 8; // 熵位数
const CS = ENT / 32; // 校验和位数
const totalBits = ENT + CS; // 总位数 = 11 的倍数
if (![128, 160, 192, 224, 256].includes(ENT)) {
throw new Error('Entropy must be 128/160/192/224/256 bits');
}
// 计算 SHA-256 并取前 CS 位作为校验和
// 教学简化:假设已有 sha256 函数
const hash = sha256(new TextDecoder().decode(entropy));
const hashBits = BigInt('0x' + hash).toString(2).padStart(256, '0');
const checksumBits = hashBits.slice(0, CS);
// 将完整数据转为二进制字符串
let dataBits = '';
for (const byte of entropy) {
dataBits += byte.toString(2).padStart(8, '0');
}
dataBits += checksumBits;
// 每 11 位 = 一个词
const words: string[] = [];
for (let i = 0; i < dataBits.length; i += 11) {
const index = parseInt(dataBits.slice(i, i + 11), 2);
words.push(this.wordList[index]);
}
return words.join(' ');
}
/**
* 助记词 → 种子(通过 PBKDF2 密钥派生)
* 真实现象中应使用该函数结合盐值 "mnemonic" 迭代 2048 次
*/
mnemonicToSeed(mnemonic: string, passphrase: string = ''): Uint8Array {
// 教学简化:实际应使用 PBKDF2-HMAC-SHA512
// 标准: seed = PBKDF2(mnemonic, "mnemonic" + passphrase, 2048, 512)
const normalized = mnemonic.normalize('NFKD');
const salt = ('mnemonic' + passphrase).normalize('NFKD');
// 返回 512 位种子
return secureRandomBytes(64); // 占位:真实需 PBKDF2 实现
}
}
// --- BIP-39 验证 ---
const bip39 = new BIP39();
const entropy = secureRandomBytes(16); // 128 位 = 12 词
console.log(`\n128 位熵: ${Array.from(entropy).map(b => b.toString(16).padStart(2, '0')).join('')}`);
// const mnemonic = bip39.entropyToMnemonic(entropy);
// console.log(`助记词: ${mnemonic}`);助记词的安全强度
| 助记词长度 | 熵位数 | 暴力尝试次数 | 安全性 |
|---|---|---|---|
| 12 词 | 128 位 | 极安全(当前算力不可行) | |
| 15 词 | 160 位 | 极高 | |
| 18 词 | 192 位 | 后量子安全级别 | |
| 21 词 | 224 位 | 过度安全 | |
| 24 词 | 256 位 | 过度安全 |
标准推荐:12 词(128 位)对大多数用户足够安全且便于记忆/抄写。24 词主要用于要求极致安全的场景(如机构冷存储)。
2.6.3 BIP-32/BIP-44:层级确定性钱包(HD Wallet)
为什么需要 HD 钱包?
传统钱包为每笔交易生成独立随机私钥。用户需要备份每一个私钥——使用 100 次 = 备份 100 个私钥。HD 钱包(BIP-32, 2012)解决了这个灾难:从一个主种子通过确定性算法派生无限多个子密钥,只需备份一次种子(助记词)。
核心思想:扩展密钥 + 子密钥派生
主种子 (64 字节, 512 位)
│
▼
┌──────────────┐
│ HMAC-SHA512 │
│ key="Bitcoin seed" │
└──────────────┘
│
├─ 主私钥 (256 位) → 主公钥
└─ 主链码 (256 位) → 用于子密钥派生子密钥派生函数(CKD, Child Key Derivation):
输出分为两半:左半部分作为子私钥,右半部分作为子链码。通过递增索引 ,可以生成无限多个独立的子密钥。
/**
* 简化的 BIP-32 层级派生(教学演示)
*/
class HDNode {
privateKey: bigint | null; // 仅"扩展私钥"节点持有
publicKey: ECPoint;
chainCode: Uint8Array;
depth: number;
index: number;
parentFingerprint: number;
constructor(
privateKey: bigint | null,
publicKey: ECPoint,
chainCode: Uint8Array,
depth: number = 0,
index: number = 0,
parentFingerprint: number = 0,
) {
this.privateKey = privateKey;
this.publicKey = publicKey;
this.chainCode = chainCode;
this.depth = depth;
this.index = index;
this.parentFingerprint = parentFingerprint;
}
/**
* 从种子创建 HD 钱包根节点
*/
static fromSeed(seed: Uint8Array): HDNode {
// 真实实现: I = HMAC-SHA512(key="Bitcoin seed", data=seed)
// 简化演示
const I = secureRandomBytes(64); // 占位
const IL = I.slice(0, 32); // 主私钥
const IR = I.slice(32, 64); // 主链码
let masterKey = 0n;
for (let i = 0; i < 32; i++) {
masterKey = (masterKey << 8n) | BigInt(IL[i]);
}
const pubKey = scalarMultiply(masterKey, new ECPoint(G.x, G.y), 0n, SECP256K1.p);
return new HDNode(masterKey, pubKey, IR, 0, 0, 0);
}
/**
* 派生子节点(简化版,省略硬化派生的细节)
*/
derive(index: number): HDNode {
if (this.depth >= 255) throw new Error('Max depth exceeded');
// 真实实现需根据 hardened (index >= 2^31) 或 normal 模式选择不同的派生方式
const childKey = (this.privateKey! + BigInt(index)) % SECP256K1.n; // 教学简化
const childPubKey = scalarMultiply(childKey, new ECPoint(G.x, G.y), 0n, SECP256K1.p);
return new HDNode(
childKey,
childPubKey,
this.chainCode, // 简化:实际需重新计算
this.depth + 1,
index,
0, // 简化:实际需计算父节点指纹
);
}
}
// --- 演示:从种子到无限地址 ---
const seed = secureRandomBytes(64);
const root = HDNode.fromSeed(seed);
console.log(`\n=== BIP-32 链式推导 ===`);
console.log(`根公钥: ${root.publicKey.toString()}`);
const child0 = root.derive(0);
console.log(`派生子 0: ${child0.publicKey.toString()}`);
const child1 = root.derive(1);
console.log(`派生子 1: ${child1.publicKey.toString()}`);
// 同样的种子总是产生同样的子密钥(确定性)BIP-44:标准化的派生路径
BIP-44 定义了五层派生路径标准:
| 层级 | 含义 | 示例 |
|---|---|---|
m | 主密钥 | |
44' | 目的:BIP-44 标准 | 固定 |
0' | 币种类型:0=比特币,60=以太坊,501=Solana | |
0' | 账户编号:0 开始 | 多账户管理 |
0 | 外部链(0=收款,1=找零) | 比特币找零机制 |
0 | 地址索引 | 每用一次 +1 |
示例路径:
- 比特币首个外部地址:
m/44'/0'/0'/0/0 - 以太坊首个地址:
m/44'/60'/0'/0/0 - 比特币第 10 个外部地址:
m/44'/0'/0'/0/9
/**
* BIP-44 路径解析器
*/
function parseBIP44Path(path: string): number[] {
const parts = path.split('/');
if (parts[0] !== 'm') throw new Error('Path must start with m');
const indices: number[] = [];
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
const isHardened = part.endsWith("'");
const index = parseInt(isHardened ? part.slice(0, -1) : part, 10);
const finalIndex = isHardened ? index + 0x80000000 : index;
indices.push(finalIndex);
}
return indices;
}
console.log(`\nBIP-44 路径 m/44'/0'/0'/0/0 解析: [${parseBIP44Path("m/44'/0'/0'/0/0").join(', ')}]`);
// 硬化索引通过在第 31 位加 1 区分:0x80000000 = 2^312.6.4 从公钥到地址:比特币与以太坊的差异
比特币地址(P2PKH)
04前缀(未压缩公钥)→SHA256→RIPEMD160(20 字节哈希)→ 加版本字节0x00(主网)→Base58Check编码。- 以 "1" 开头的是主网 P2PKH 地址(如
1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa)。
以太坊地址
- 取公钥(64 字节未压缩,去掉
0x04)的Keccak-256哈希,取最后 20 字节。 - 以
0x开头,40 个十六进制字符(如0xdAC17F958D2ee523a2206206994597C13D831ec7)。 - 无 Base58Check——原始十六进制,但包含 EIP-55 大小写校验。
/**
* 公钥到地址的两种路线
*/
function bitcoinAddressFromPubkey(pubKey: ECPoint): string {
// 简化:仅展示流程
const pubKeyBytes = new Uint8Array(65); // 0x04 + x + y
// ... 序列化公钥位为字节
// hash160 = RIPEMD160(SHA256(pubKeyBytes))
// address = Base58Check(0x00 || hash160)
return "1A1z...P1eP"; // 占位
}
function ethereumAddressFromPubkey(pubKey: ECPoint): string {
// 未压缩公钥去掉 0x04 后的 64 字节
const pubKeyBytes = new Uint8Array(64);
// ... 序列化 x, y 为字节
// address = Keccak256(pubKeyBytes).slice(-20)
return "0x..."; // 占位
}| 特性 | 比特币 P2PKH | 以太坊 |
|---|---|---|
| 哈希函数 | SHA256 → RIPEMD160 | Keccak-256 |
| 输出大小 | 160 位 | 160 位(Keccak-256 的最后 160 位) |
| 编码 | Base58Check | 十六进制(EIP-55 大小写校验) |
| 前缀 | "1"(主网) | "0x" |
| 大小写敏感 | 不敏感 | EIP-55 校验依赖大小写 |
2.6.5 冷存储与硬件钱包的安全实践
威胁模型
| 威胁 | 热钱包(联网软件) | 冷钱包(离线) |
|---|---|---|
| 操作系统漏洞/恶意软件 | ❌ 高风险 | ✅ 免疫 |
| 网络钓鱼 | ❌ 可能误签 | ✅ 需物理确认 |
| 物理盗窃 | ❓ 依赖设备密码 | ❓ 依赖物理安全 |
| 用户误操作 | 高 | 高(需确认多个步骤) |
硬件钱包的核心机制
硬件钱包(如 Ledger、Trezor)的核心安全保证是:
私钥从不出现在硬件设备的"易泄露区域"(RAM/CPU 缓存可能被侧信道攻击),且永远不暴露给连接的主机。
交易签名流程:
- 主机将待签名的交易数据发送给硬件设备。
- 硬件在屏幕上显示交易内容(如"发送 0.5 BTC 到 1A1z...")。
- 用户物理按下设备上的确认按钮。
- 设备使用芯片内安全元素的私钥执行签名。
- 只将签名结果 返回给主机,私钥从未离开设备。
助记词的安全备份
永远不要:
- 将助记词存储在联网设备(手机照片、云盘、邮件)。
- 将助记词输入任何网站或应用(除非是首次在新设备上恢复钱包)。
- 只保留一份备份(单点故障)。
推荐做法:
- 写在金属板(防火防水)上,存放在两个不同物理位置。
- 或使用Shamir 秘密共享(BIP-39 扩展):将 24 词分成 3 份,任意 2 份即可恢复,避免单点失窃/丢失。
核心认知
- 钱包 = 密钥管理器。 它不"存放"币,币永远在区块链上。钱包只是保存了授权花费这些币的私钥。
- BIP-39 助记词是人类可记忆的 128 位熵。 校验和机制保证 12 个词中抄错任意一个词可以立即被检测(约 1/256 的错误会被校验和捕获,约 255/256 的错误会被词表检查捕获)。
- BIP-32 推导 = 一次备份,无限密钥。 主种子派生子密钥的树状结构,使得企业可以批量管理数千个客户存款地址,个人可以在多账户间隔离隐私。
- 冷存储的核心不是技术,而是物理隔离。 硬件钱包的价值在于"私钥永远不会暴露在联网环境中"。空气隔离的计算机 + 离线签名 + 二维码传输,是机构级冷存储的标准做法。
下一预告:2.7 节将探索默克尔树(Merkle Tree)——如何将数百万笔交易压缩为一个 32 字节的根哈希,以及轻客户端(SPV)如何在不下载完整区块链的情况下验证交易存在性。
评论
0评论加载中…