- 作者:老汪软件技巧
- 发表时间:2024-09-22 07:01
- 浏览量:
BIP-32 和 BIP-39 的基本概念
BIP-32 和 BIP-39 是比特币改进提案中的两个标准,它们都与加密货币钱包的密钥管理和生成相关。
BIP-32:分层确定性钱包(HD Wallets):
HD钱包(Hierarchical Deterministic Wallet,分层确定性钱包)是一种加密货币钱包,通过BIP-32标准生成一棵树状结构的钱包密钥。HD钱包的特点是可以从一个称为“种子(seed)”的初始值生成一系列的私钥和公钥对,从而生成多个地址。
BIP-39:助记词(Mnemonic)标准
BIP-39 主要用于 将随机生成的熵转换为一串助记词,以便于人类记忆和备份。BIP-39 定义了一种将复杂的种子数据表示为简单、易于记忆的助记词(如 12 或 24 个单词)的标准。
随机生成一定数量的二进制熵(如 128 位或 256 位)。将这些熵映射到一个固定的助记词列表中,生成 12 或 24 个单词。助记词经过 PBKDF2 哈希算法处理生成一个种子,这个种子用于 BIP-32 钱包的密钥生成。生成助记词的步骤:1、生成随机熵(Entropy)
我们使用 typescript 来封装一个生成随机熵的函数:
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 位。将这个校验位附加到熵的末尾,形成新的二进制序列。
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);
}
具体分析如下:
计算熵的长度(以位为单位) :
const ENT = entropy.length * 8;
这里的 entropy.length 是熵的字节长度,乘以8将其转换为位长度。这一步计算获得了熵的总位数(ENT)。
计算校验和的位数(checksum length) :
const CS = ENT / 32;
校验位数是熵总位数的 1/32。比如,如果熵长度是 256 位,那么校验和位数就是 256 / 32 = 8 位。
使用 SHA-256 哈希算法对熵进行哈希:
const hash = crypto.createHash('sha256').update(entropy).digest();
将熵作为输入数据,计算其 SHA-256 哈希值。结果 hash 是一个包含哈希值(32字节)的 Buffer 对象。
提取校验位:
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)组合成一个二进制字符串。
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;
}
初始化空字符串:
let binaryString = '';
这个变量 binaryString 用来存储最终的拼接结果,包括熵和校验位的二进制表示。
转换熵为二进制字符串:
for (const byte of entropy) {
binaryString += byte.toString(2).padStart(8, '0');
}
计算校验位数(checkbits length,简称CS) :
const CS = (entropy.length * 8) / 32;
转换校验位为二进制字符串并拼接到最终结果中:
binaryString += checkBits.toString(2).padStart(CS, '0');
返回最终的二进制字符串:
return binaryString;
4、将二进制字符串进行分组
将生成的熵加校验位的二进制序列按照每组 11 位分割。例如,对于 128 位熵和 4 位校验位,二进制序列长度为 132 位,这将分成 12 组(每组 11 位)。好的,下面我们重新解析并解释你优化后的 splitIntoIndices 函数。你增强了函数的验证逻辑,确保最终生成的索引数与预期的数量一致,这样能更好地处理输入错误或非标准输入的情况。
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;
}
初始化一个空数组:
const indices = [];
获取二进制字符串的总长度:
const totalBits = bits.length;
这条语句得到字符串 bits 的总长度,并存储在 totalBits 变量中。
计算应该生成的索引数量:
const wordCount = totalBits / 11;
通过将总长度 totalBits 除以11,计算出应该生成的索引数 wordCount。因为每个索引对应11位二进制数,所以字符串的总长度必须是11的倍数。
遍历二进制字符串,每次处理11位:
for (let i = 0; i < totalBits; i += 11) {
循环从 i 为0开始,每次增加11,直到 i 达到或超过 totalBits。
截取当前的11位子字符串并转换为整数:
const index = parseInt(bits.slice(i, i + 11), 2);
bits.slice(i, i + 11)从字符串 bits 的位置 i 截取11位长度的子字符串。如果 i + 11 超过字符串长度,slice 方法会自动截取到字符串的末尾。
parseInt(..., 2)将截取的子字符串从二进制字符串转换为十进制整数。
将结果添加到数组中:
indices.push(index);
将上一步得到的整数 index 添加到 indices 数组中。
验证生成的索引数量是否与预期一致:
if (indices.length !== wordCount) {
throw new Error(
`Invalid number of indices generated. Expected ${wordCount}, but got ${indices.length}`
);
}
在循环完成后,检查生成的索引数量是否与预期的数量 wordCount 一致。如果不一致,则抛出一个错误,说明输入的二进制字符串长度可能不是11的倍数或有其他问题。
返回结果数组:
return indices;
验证通过后,返回包含所有转换结果的数组 indices。
示例:
如果输入 bits = "000000000010000000010010000110010111",长度为33位:
经过循环:
生成的 indices 数组为 [1, 18, 811]。
如果 indices.length 和 wordCount 都是3,那么验证通过,最终返回数组 [1, 18, 811]。
5、将索引映射为助记词:
function indicesToMnemonic(indices: number[]): string {
// 获取 BIP39 英文单词列表
const wordlist = bip39.wordlists.english;
// 将索引映射为单词并以空格连接成字符串
return indices.map(index => wordlist[index]).join(' ');
}
获取 BIP39 单词列表:
const wordlist = bip39.wordlists.english;
将索引映射为单词:
return indices.map(index => wordlist[index]).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)来生成种子,并可以防止攻击者通过计算快速破解密钥。
生成过程
助记词和密码短语组合:
PBKDF2 处理:PBKDF2 的输入包含以下几个部分:
具体步骤
组合助记词和密码短语:
设置盐值:
PBKDF2 处理:
示例代码
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:/MagicalBrid…go: /MagicalBrid…