BIP-32 和 BIP-39 的基本概念
BIP-32 和 BIP-39 是比特币改进提案中的两个标准,它们都与加密货币钱包的密钥管理和生成相关。
BIP-32:分层确定性钱包(HD Wallets):
HD钱包(Hierarchical Deterministic Wallet,分层确定性钱包)是一种加密货币钱包,通过BIP-32标准生成一棵树状结构的钱包密钥。HD钱包的特点是可以从一个称为"种子(seed)"的初始值生成一系列的私钥和公钥对,从而生成多个地址。
- 作用:BIP-32 提供了一种从一个主密钥(称为种子或根密钥)生成一系列子密钥的机制。
- 特性:它支持创建具有分层结构的子密钥对。例如,可以生成用于不同账户、地址或应用的子密钥,从而保持不同交易的独立性和隐私。
- 路径结构 :BIP-32 使用一种树形结构的路径格式,如
m/44'/60'/0'/0/0
,每个层级可以代表一个特定的用途,用户可以通过指定不同的路径生成不同的子密钥。 - 好处:只需备份一个种子密钥,便可以恢复整棵树上的所有密钥。提高了可扩展性和管理密钥的便捷性。
BIP-39:助记词(Mnemonic)标准
BIP-39 主要用于 将随机生成的熵转换为一串助记词,以便于人类记忆和备份。BIP-39 定义了一种将复杂的种子数据表示为简单、易于记忆的助记词(如 12 或 24 个单词)的标准。
- 作用:BIP-39 提供了一种人类可读的方式来备份 HD 钱包的种子。它通过使用一组预定义的单词列表生成一串助记词,从而更容易备份和恢复钱包。
- 助记词生成过程:
-
- 随机生成一定数量的二进制熵(如 128 位或 256 位)。
- 将这些熵映射到一个固定的助记词列表中,生成 12 或 24 个单词。
- 助记词经过 PBKDF2 哈希算法处理生成一个种子,这个种子用于 BIP-32 钱包的密钥生成。
- 应用场景:BIP-39 通常用于生成可以用作 BIP-32 种子的助记词。在创建 HD 钱包时,用户通常会得到一串助记词,这些助记词就是基于 BIP-39 标准生成的。
生成助记词的步骤:
1、生成随机熵(Entropy)
- 首先生成一段随机的二进制数据,称为 熵 。这个熵的长度通常为 128 位、160 位、192 位、224 位或 256 位。128 位熵 对应生成 12 个助记词。256 位熵 对应生成 24 个助记词。
- 这一步非常重要,因为生成的熵决定了整个钱包的安全性,熵的随机性应由强随机数生成器(CSPRNG)来保证。
我们使用 typescript 来封装一个生成随机熵的函数:
javascript
import * as crypto from 'crypto';
function generateEntropy(bitSize: 128 | 160 | 192 | 224 | 256 = 128): Buffer {
if (![128, 160, 192, 224, 256].includes(bitSize)) {
throw new Error(
'Invalid entropy bit size, should be one of 128, 160, 192, 224, or 256.'
);
}
return crypto.randomBytes(bitSize / 8);
}
这个 generateEntropy
函数用于生成指定大小的随机熵(entropy),返回一个包含随机字节的 Buffer
对象。以下是对函数的逐步解析:
参数 :bitSize
: 该参数指定生成熵的位数,可以是 128、160、192、224 或 256。默认值为 128。
输入验证 :使用 Array.includes
方法检查 bitSize
是否在允许的值(128, 160, 192, 224, 256)中。如果不在范围内,函数会抛出一个错误,提示"无效的熵位大小"。
生成随机字节 :crypto.randomBytes(bitSize / 8)
:根据 bitSize
计算字节数(位数除以 8),并调用 crypto
模块的 randomBytes
方法生成随机字节。返回值是一个 Buffer
对象,包含生成的随机字节。
示例用法: 调用 generateEntropy(256)
将生成 32 个随机字节,调用 generateEntropy(128)
将生成 16 个随机字节。
错误处理: 如果传入无效的 bitSize
,函数会抛出异常,确保函数的使用是安全和可靠的。
2、计算校验位:
为了验证助记词是否正确,BIP-39 将为熵添加一个校验位,校验位由熵的前 entropy_length / 32
位构成。例如,对于 128 位熵,校验位的长度为 128/32 = 4
位;对于 256 位熵,校验位长度为 8 位。将这个校验位附加到熵的末尾,形成新的二进制序列。
ini
function calculateChecksum(entropy: Buffer): number {
const ENT = entropy.length * 8;
const CS = ENT / 32;
const hash = crypto.createHash('sha256').update(entropy).digest();
return hash[0] >>> (8 - CS);
}
具体分析如下:
- 计算熵的长度(以位为单位) :
ini
const ENT = entropy.length * 8;
这里的 entropy.length
是熵的字节长度,乘以8将其转换为位长度。这一步计算获得了熵的总位数(ENT)。
- 计算校验和的位数(checksum length) :
ini
const CS = ENT / 32;
校验位数是熵总位数的 1/32。比如,如果熵长度是 256 位,那么校验和位数就是 256 / 32 = 8 位。
- 使用 SHA-256 哈希算法对熵进行哈希:
ini
const hash = crypto.createHash('sha256').update(entropy).digest();
将熵作为输入数据,计算其 SHA-256 哈希值。结果 hash
是一个包含哈希值(32字节)的 Buffer
对象。
- 提取校验位:
bash
return hash[0] >>> (8 - CS);
Hash 的第一个字节是 hash[0]
。SHA-256 产生的哈希值是256位(32字节),我们取第一个字节(8位)。在这一字节中,>>>
是无符号右移运算符,它会将 hash[0] 向右位移 (8 - CS) 位,并将左侧用零填充。
如果打个比方的话,假设我们有一串数字 12345678(对应一个字节的8位二进制位),我们需要取前 CS 位。
例如,CS 为 4 时,我们计算 8 - 4 = 4,然后将整个数字向右移 4 位(得到 00001234 的形式),最右边四个位置上就存储了我们需要的前 CS 位,这些就是我们要提取的校验位。
3、将熵和校验位组合起来
该函数的功能是将给定的熵(entropy)和校验位(checksumBits)组合成一个二进制字符串。
csharp
function combineEntropyAndCheckBitsToBinary(
entropy: Buffer,
checksumBits: number
): string {
// 初始化一个空的二进制字符串
let binaryString = '';
// 将熵中的每个字节转换为二进制字符串,并连接起来
for (const byte of entropy) {
// 将字节转换为二进制字符串,不足8位的用0填充(padStart)
binaryString += byte.toString(2).padStart(8, '0');
}
// 计算校验和位数(checksum length)
const CS = (entropy.length * 8) / 32;
// 将校验位转换为二进制字符串,不足CS位的用0填充
binaryString += checksumBits.toString(2).padStart(CS, '0');
// 返回组合后的二进制字符串
return binaryString;
}
初始化空字符串:
ini
let binaryString = '';
这个变量 binaryString
用来存储最终的拼接结果,包括熵和校验位的二进制表示。
转换熵为二进制字符串:
arduino
for (const byte of entropy) {
binaryString += byte.toString(2).padStart(8, '0');
}
for (const byte of entropy)
:依次遍历entropy
这个 Buffer 中的每一个字节。byte.toString(2)
:将每个字节转换为二进制字符串(不包括前导的0b
)。.padStart(8, '0')
:通过padStart
方法确保每个二进制字符串至少有8位。不足8位的左侧用'0'填充。binaryString += ...
:将每个字节的二进制表示追加到binaryString
中。
计算校验位数(checkbits length,简称CS) :
ini
const CS = (entropy.length * 8) / 32;
entropy.length * 8
:计算熵的总位数(将字节长度转换为位长度)。/ 32
:计算校验和的位数。根据BIP39规范,校验和的长度是熵位数的1/32。
转换校验位为二进制字符串并拼接到最终结果中:
scss
binaryString += checkBits.toString(2).padStart(CS, '0');
checkBits.toString(2)
:将校验位(整数)转换为二进制字符串。.padStart(CS, '0')
:确保二进制字符串的长度至少为CS
位。不足CS
位的左侧用'0'填充。binaryString += ...
:将校验位的二进制表示追加到binaryString
中。
- 返回最终的二进制字符串:
kotlin
return binaryString;
4、将二进制字符串进行分组
将生成的熵加校验位的二进制序列按照每组 11 位分割。例如,对于 128 位熵和 4 位校验位,二进制序列长度为 132 位,这将分成 12 组(每组 11 位)。好的,下面我们重新解析并解释你优化后的 splitIntoIndices
函数。你增强了函数的验证逻辑,确保最终生成的索引数与预期的数量一致,这样能更好地处理输入错误或非标准输入的情况。
typescript
function splitIntoIndices(bits: string): number[] {
// 初始化一个空数组,用来存储转换后的数字索引
const indices = [];
// 获取二进制字符串的总长度
const totalBits = bits.length;
// 计算应该生成的索引数量
const wordCount = totalBits / 11;
// 遍历二进制字符串,每次处理11位
for (let i = 0; i < totalBits; i += 11) {
// 从当前位置截取11位子字符串,并将其转换为整数
const index = parseInt(bits.slice(i, i + 11), 2);
// 将转换后的整数添加到结果数组
indices.push(index);
}
// 验证生成的索引数量是否与预期一致
if (indices.length !== wordCount) {
throw new Error(
`Invalid number of indices generated. Expected ${wordCount}, but got ${indices.length}`
);
}
// 返回结果数组
return indices;
}
初始化一个空数组:
ini
const indices = [];
- 这个数组
indices
用来存储最后生成的整数索引。
获取二进制字符串的总长度:
ini
const totalBits = bits.length;
这条语句得到字符串 bits
的总长度,并存储在 totalBits
变量中。
计算应该生成的索引数量:
ini
const wordCount = totalBits / 11;
通过将总长度 totalBits
除以11,计算出应该生成的索引数 wordCount
。因为每个索引对应11位二进制数,所以字符串的总长度必须是11的倍数。
遍历二进制字符串,每次处理11位:
css
for (let i = 0; i < totalBits; i += 11) {
循环从 i
为0开始,每次增加11,直到 i
达到或超过 totalBits
。
截取当前的11位子字符串并转换为整数:
ini
const index = parseInt(bits.slice(i, i + 11), 2);
bits.slice(i, i + 11)
从字符串 bits
的位置 i
截取11位长度的子字符串。如果 i + 11
超过字符串长度,slice
方法会自动截取到字符串的末尾。
parseInt(..., 2)
将截取的子字符串从二进制字符串转换为十进制整数。
将结果添加到数组中:
ini
indices.push(index);
将上一步得到的整数 index
添加到 indices
数组中。
验证生成的索引数量是否与预期一致:
javascript
if (indices.length !== wordCount) {
throw new Error(
`Invalid number of indices generated. Expected ${wordCount}, but got ${indices.length}`
);
}
在循环完成后,检查生成的索引数量是否与预期的数量 wordCount
一致。如果不一致,则抛出一个错误,说明输入的二进制字符串长度可能不是11的倍数或有其他问题。
返回结果数组:
kotlin
return indices;
验证通过后,返回包含所有转换结果的数组 indices
。
示例:
如果输入 bits = "000000000010000000010010000110010111"
,长度为33位:
totalBits
为 33。wordCount
为 33 / 11 = 3。
经过循环:
- 第一次截取
bits.slice(0, 11)
得到"00000000001"
,转换为整数1
。 - 第二次截取
bits.slice(11, 22)
得到"00000010010"
,转换为整数18
。 - 第三次截取
bits.slice(22, 33)
得到"00011001011"
,转换为整数811
。
生成的 indices
数组为 [1, 18, 811]
。
如果 indices.length
和 wordCount
都是3,那么验证通过,最终返回数组 [1, 18, 811]
。
5、将索引映射为助记词:
- BIP-39 提供了一个包含 2048 个常用单词的助记词表(2048 个单词的词汇表),每个单词都有唯一的编号(0 到 2047)。
- 将每组 11 位二进制数映射到助记词表中的对应单词。由于 11 位二进制数的值范围是 0 到 2047,正好与助记词表的 2048 个单词一一对应。
typescript
function indicesToMnemonic(indices: number[]): string {
// 获取 BIP39 英文单词列表
const wordlist = bip39.wordlists.english;
// 将索引映射为单词并以空格连接成字符串
return indices.map(index => wordlist[index]).join(' ');
}
获取 BIP39 单词列表:
ini
const wordlist = bip39.wordlists.english;
- 这一行代码中,
bip39.wordlists.english
获取了 BIP39 定义的英语单词列表。 - BIP39 是一个用于生成和验证加密货币钱包助记符短语的标准,其中定义了一组2048个唯一的短语(单词)。
wordlist
是一个包含2048个单词的数组,每个单词对应一个索引,从0到2047。
将索引映射为单词:
ini
return indices.map(index => wordlist[index]).join(' ');
indices.map(index => wordlist[index])
使用map
函数将indices
数组中的每个索引转换为相应索引的单词。map
函数的参数是一个回调函数index => wordlist[index]
,这个回调函数会对数组中的每个索引进行操作,并返回对应的单词。- 每个索引
index
在wordlist
中都有一个对应的单词wordlist[index]
。
将单词连接成字符串:
bash
.join(' ')
- 前一步生成的单词数组将通过
join(' ')
连接成一个以空格分隔的字符串,从而形成助记符短语。 join
函数将数组中的所有元素按指定的分隔符(这里是空格)连接成一个字符串。
假设输入的 indices
数组为 [0, 1, 2, 3]
:
wordlist[0]
:假设是"abandon"
,wordlist[1]
:假设是"ability"
,wordlist[2]
:假设是"able"
,wordlist[3]
:假设是"about"
。
则 indices.map(index => wordlist[index])
会生成一个数组 ["abandon", "ability", "able", "about"]
。
最终通过 join(' ')
连接后,返回的字符串结果为 "abandon ability able about"
。
助记词结合密码短语:
助记词还可以与一个 密码短语(passphrase)组合使用来提高安全性。助记词和密码短语经过 PBKDF2 函数处理后生成最终的种子(seed),从而用于钱包的生成。这种方式提供了额外的保护,即使助记词被泄露,没有密码短语也无法恢复钱包。
助记词与密码短语
助记词和密码短语都是用于生成钱包种子的输入数据。助记词是由一组单词组成的短语,密码短语是用户自己添加的额外字符串,提供额外的安全层。
PBKDF2 函数
PBKDF2 (Password-Based Key Derivation Function 2) 是一种基于密码的密钥派生函数,用于增强安全性。它通过多次迭代哈希函数(如 HMAC-SHA512)来生成种子,并可以防止攻击者通过计算快速破解密钥。
生成过程
助记词和密码短语组合:
- 助记词短语是一组单词(通常是12到24个单词)。
- 密码短语是一段由用户自定义的字符串,可以为空。
PBKDF2 处理:PBKDF2 的输入包含以下几个部分:
- 助记词短语:作为主要输入数据。
- 盐值:由固定字符串 "mnemonic" 和可选的密码短语组成。
- 迭代次数:通常是2048次。
- 派生密钥长度:生成结果的长度,通常是512位(64字节)。
具体步骤
组合助记词和密码短语:
- 助记词,如
"abandon ability able about"
。 - 密码短语,如
"mySecurePassphrase"
(如果没有可以为空)。
设置盐值:
- 盐值 =
"mnemonic" + 密码短语
。 - 如果密码短语是
"mySecurePassphrase"
,则盐值为"mnemonicmySecurePassphrase"
。 - 如果密码短语是空字符串,盐值为
"mnemonic"
。
PBKDF2 处理:
- 使用助记词短语作为一个单独的字符串(用空格连接的单词)作为输入数据。
- 使用设定的盐值。
- 迭代次数:2048。
- 哈希函数:HMAC-SHA512。
- 密钥长度:512位(64字节)。
示例代码
ini
const crypto = require('crypto');
function mnemonicToSeed(mnemonic: string, passphrase: string = ''): Buffer {
const mnemonicBuffer = Buffer.from(mnemonic, 'utf8');
const salt = Buffer.from('mnemonic' + passphrase, 'utf8');
const seed = crypto.pbkdf2Sync(mnemonicBuffer, salt, 2048, 64, 'sha512');
return seed;
}
// 示例使用
const mnemonic = "abandon ability able about"; // 示例助记词
const passphrase = "mySecurePassphrase"; // 示例密码短语
const seed = mnemonicToSeed(mnemonic, passphrase);
console.log(seed.toString('hex')); // 打印种子值
过程解析:
助记词短语 :输入:mnemonic = "abandon ability able about"
可选密码短语 :输入:passphrase = "mySecurePassphrase"
设定盐值 :salt = "mnemonic" + passphrase
即,salt = "mnemonicmySecurePassphrase"
使用 PBKDF2 进行密钥派生:基于输入的助记词和盐值,进行2048次 HMAC-SHA512 迭代计算。生成长度为64字节(512位)的种子。
输出:
seed
是最后生成的钱包种子,它可以用于导出各种类型的加密货币钱包私钥。这一过程确保了钱包种子的高安全性,即使助记词被攻击者获得,没有正确的密码短语也难以生成正确的种子。
总结:
通过将助记词和密码短语结合并使用 PBKDF2 算法处理,可以生成高度安全的钱包种子。这种方法通过增加计算复杂性和密码短语的组合,提高了种子的安全性,从而提升了加密货币钱包的安全防护能力。
完整代码链接:
node:github.com/MagicalBrid... go: github.com/MagicalBrid...