手把手教你实现HD钱包助记词生成算法

  • Louis
  • 更新于 2024-09-22 14:44
  • 阅读 782

BIP-32和BIP-39的基本概念BIP-32和BIP-39是比特币改进提案中的两个标准,它们都与加密货币钱包的密钥管理和生成相关。BIP-32:分层确定性钱包(HDWallets):HD钱包(HierarchicalDeterministicWallet,分层确定性钱包)


theme: channing-cyan

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 钱包的种子。它通过使用一组预定义的单词列表生成一串助记词,从而更容易备份和恢复钱包。
  • 助记词生成过程
  1. 随机生成一定数量的二进制熵(如 128 位或 256 位)。
  2. 将这些熵映射到一个固定的助记词列表中,生成 12 或 24 个单词。
  3. 助记词经过 PBKDF2 哈希算法处理生成一个种子,这个种子用于 BIP-32 钱包的密钥生成。
  • 应用场景:BIP-39 通常用于生成可以用作 BIP-32 种子的助记词。在创建 HD 钱包时,用户通常会得到一串助记词,这些助记词就是基于 BIP-39 标准生成的。

生成助记词的步骤:

1、生成随机熵(Entropy)

  • 首先生成一段随机的二进制数据,称为 。这个熵的长度通常为 128 位、160 位、192 位、224 位或 256 位。128 位熵 对应生成 12 个助记词。256 位熵 对应生成 24 个助记词。
  • 这一步非常重要,因为生成的熵决定了整个钱包的安全性,熵的随机性应由强随机数生成器(CSPRNG)来保证。

我们使用 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);
}

具体分析如下:

  1. 计算熵的长度(以位为单位)
const ENT = entropy.length * 8;

这里的 entropy.length 是熵的字节长度,乘以8将其转换为位长度。这一步计算获得了熵的总位数(ENT)。

  1. 计算校验和的位数(checksum length)
const CS = ENT / 32;

校验位数是熵总位数的 1/32。比如,如果熵长度是 256 位,那么校验和位数就是 256 / 32 = 8 位。

  1. 使用 SHA-256 哈希算法对熵进行哈希
const hash = crypto.createHash('sha256').update(entropy).digest();

将熵作为输入数据,计算其 SHA-256 哈希值。结果 hash 是一个包含哈希值(32字节)的 Buffer 对象。

  1. 提取校验位
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');
}
  • 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 中。
  1. 返回最终的二进制字符串
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 = [];
  • 这个数组 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.lengthwordCount 都是3,那么验证通过,最终返回数组 [1, 18, 811]

5、将索引映射为助记词:

  • BIP-39 提供了一个包含 2048 个常用单词的助记词表(2048 个单词的词汇表),每个单词都有唯一的编号(0 到 2047)。
  • 将每组 11 位二进制数映射到助记词表中的对应单词。由于 11 位二进制数的值范围是 0 到 2047,正好与助记词表的 2048 个单词一一对应。
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 定义的英语单词列表。
  • BIP39 是一个用于生成和验证加密货币钱包助记符短语的标准,其中定义了一组2048个唯一的短语(单词)。
  • wordlist 是一个包含2048个单词的数组,每个单词对应一个索引,从0到2047。

将索引映射为单词

return indices.map(index => wordlist[index]).join(' ');
  • indices.map(index => wordlist[index]) 使用 map 函数将 indices 数组中的每个索引转换为相应索引的单词。
  • map 函数的参数是一个回调函数 index => wordlist[index],这个回调函数会对数组中的每个索引进行操作,并返回对应的单词。
  • 每个索引 indexwordlist 中都有一个对应的单词 wordlist[index]

将单词连接成字符串

.join(' ')
  • 前一步生成的单词数组将通过 join(' ') 连接成一个以空格分隔的字符串,从而形成助记符短语。
  • join 函数将数组中的所有元素按指定的分隔符(这里是空格)连接成一个字符串。

假设输入的 indices 数组为 [0, 1, 2, 3]

  1. wordlist[0]:假设是 "abandon"
  2. wordlist[1]:假设是 "ability"
  3. wordlist[2]:假设是 "able"
  4. 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字节)。

示例代码

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

点赞 0
收藏 2
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Louis
Louis
web3 developer,技术交流或者有工作机会可加VX: magicalLouis