• 作者:老汪软件技巧
  • 发表时间: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…