BIP-32和BIP-39的基本概念BIP-32和BIP-39是比特币改进提案中的两个标准,它们都与加密货币钱包的密钥管理和生成相关。BIP-32:分层确定性钱包(HDWallets):HD钱包(HierarchicalDeterministicWallet,分层确定性钱包)
BIP-32 和 BIP-39 是比特币改进提案中的两个标准,它们都与加密货币钱包的密钥管理和生成相关。
HD钱包(Hierarchical Deterministic Wallet,分层确定性钱包)是一种加密货币钱包,通过BIP-32标准生成一棵树状结构的钱包密钥。HD钱包的特点是可以从一个称为“种子(seed)”的初始值生成一系列的私钥和公钥对,从而生成多个地址。
m/44'/60'/0'/0/0
,每个层级可以代表一个特定的用途,用户可以通过指定不同的路径生成不同的子密钥。BIP-39 主要用于 将随机生成的熵转换为一串助记词,以便于人类记忆和备份。BIP-39 定义了一种将复杂的种子数据表示为简单、易于记忆的助记词(如 12 或 24 个单词)的标准。
我们使用 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
,函数会抛出异常,确保函数的使用是安全和可靠的。
为了验证助记词是否正确,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)。
const CS = ENT / 32;
校验位数是熵总位数的 1/32。比如,如果熵长度是 256 位,那么校验和位数就是 256 / 32 = 8 位。
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 位,这些就是我们要提取的校验位。
该函数的功能是将给定的熵(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');
}
for (const byte of entropy)
:依次遍历 entropy
这个 Buffer 中的每一个字节。byte.toString(2)
:将每个字节转换为二进制字符串(不包括前导的 0b
)。.padStart(8, '0')
:通过 padStart
方法确保每个二进制字符串至少有8位。不足8位的左侧用'0'填充。binaryString += ...
:将每个字节的二进制表示追加到 binaryString
中。计算校验位数(checkbits length,简称CS) :
const CS = (entropy.length * 8) / 32;
entropy.length * 8
:计算熵的总位数(将字节长度转换为位长度)。/ 32
:计算校验和的位数。根据BIP39规范,校验和的长度是熵位数的1/32。转换校验位为二进制字符串并拼接到最终结果中:
binaryString += checkBits.toString(2).padStart(CS, '0');
checkBits.toString(2)
:将校验位(整数)转换为二进制字符串。.padStart(CS, '0')
:确保二进制字符串的长度至少为 CS
位。不足 CS
位的左侧用'0'填充。binaryString += ...
:将校验位的二进制表示追加到 binaryString
中。return binaryString;
将生成的熵加校验位的二进制序列按照每组 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 = [];
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位:
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]
。
function indicesToMnemonic(indices: number[]): string {
// 获取 BIP39 英文单词列表
const wordlist = bip39.wordlists.english;
// 将索引映射为单词并以空格连接成字符串
return indices.map(index => wordlist[index]).join(' ');
}
获取 BIP39 单词列表:
const wordlist = bip39.wordlists.english;
bip39.wordlists.english
获取了 BIP39 定义的英语单词列表。wordlist
是一个包含2048个单词的数组,每个单词对应一个索引,从0到2047。将索引映射为单词:
return indices.map(index => wordlist[index]).join(' ');
indices.map(index => wordlist[index])
使用 map
函数将 indices
数组中的每个索引转换为相应索引的单词。map
函数的参数是一个回调函数 index => wordlist[index]
,这个回调函数会对数组中的每个索引进行操作,并返回对应的单词。index
在 wordlist
中都有一个对应的单词 wordlist[index]
。将单词连接成字符串:
.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 (Password-Based Key Derivation Function 2) 是一种基于密码的密钥派生函数,用于增强安全性。它通过多次迭代哈希函数(如 HMAC-SHA512)来生成种子,并可以防止攻击者通过计算快速破解密钥。
助记词和密码短语组合:
PBKDF2 处理:PBKDF2 的输入包含以下几个部分:
组合助记词和密码短语:
"abandon ability able about"
。"mySecurePassphrase"
(如果没有可以为空)。设置盐值:
"mnemonic" + 密码短语
。"mySecurePassphrase"
,则盐值为 "mnemonicmySecurePassphrase"
。"mnemonic"
。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 算法处理,可以生成高度安全的钱包种子。这种方法通过增加计算复杂性和密码短语的组合,提高了种子的安全性,从而提升了加密货币钱包的安全防护能力。
typescript: https://github.com/MagicalBridge/blockchain_ts/blob/master/src/generate_mnemonics.ts go: https://github.com/MagicalBridge/what-the-gin/blob/master/utils/generate_mnemonics.go
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!