← 返回文章

比特币学习之 HD 钱包——从熵到助记词(bip39)

概念 助记种子:将随机数编码到单词中,并使用它们创建一个种子 助记词:将随机产生的大量数

概念

  • 助记种子:将随机数编码到单词中,并使用它们创建一个种子

  • 助记词:将随机产生的大量数字表示成一系列单词的方式,是我们更容易的存储。

这些单词然后被用来创建一个种子,这个种子在 HD 钱包中生成扩展密钥。

换句话说:我们的钱包将使用一个大的随机数来生成我们所有的密钥和地址,同时助记词给我们一个存储这个大随机数的简便方法。

那么,如何生成助记种子呢? 分三步:

  1. 生成熵

  2. 熵生成助记词

  3. 助记词生成助记种子

生成熵

熵:我们可以认为熵是一个非常大的随机数,以前从来没有人产生过,将来也不会产生。同时计算机是以二进制存储数字的,所以我们可以将这个数字看成一串二进制数(eg: 10011010010001... )

bit(0 或 1)是计算机存储的最小单位

同时熵还需要满足特定的要求:

  • 必须是32位的倍数: 这使得我们可以将熵分成偶数个块,然后转化成单词。

  • 熵必须在128位到256位之间:因为这足以使得两个人不可能产生相同的熵。

const randomBytes = require("randombytes");
var entropy = randomBytes(16) 

// 输出熵 entropy 的值
console.log(entropy.toString('hex'))
// 输出结果:
// 'fa82c8bb2fd02f82ff0114069f2e5a19' 
// 它是16进制,换算成二进制,熵长度为32*4=128

注意:我们生成随机数时,不要使用函数中默认的"random"函数。它产生的随机数不够随机,是有可能破解的。我们要使用一个安全的随机数发生器。

熵生成助记词

现在,我们将熵编码成单词(即助记词)。

首先,我们要将校验位添加到熵中,校验位可以帮我们检测熵是否在传输过程中被修改。

通过对初始熵 entropy 取SHA256散列来获得校验和(CS_length= 熵长度/32,取得到的SHA256散列的前 CS_length 位),然后将校验和附加到初始熵的末尾。

接下来,我们把最终的熵分成每11位为一组,并把它们转换成十进制数,每个组编码对应一个0-2047的数字,该数字作为一个索引到wordlist,对应获得wordlist上相应索引的值。

最后,我们将这些数字转换成单词,最终合在一起作为助记句。

助记词通常在12到24个单词之间

const createHash = require("create-hash");
const bip39 = require('bip39');
// 之前我们通过随机数生成的熵
entropy = 'fa82c8bb2fd02f82ff0114069f2e5a19'
// 将熵导入到 Buffer 中
entropy = Buffer.from(entropy, 'hex');
// 将熵 entropy 的十六进制字节转化成二进制
const entropyBits = bytesToBinary([...entropy]);
//生成 检验位
const checksumBits = deriveChecksumBits(entropy);


console.log(entropyBits)
console.log(checksumBits)
// 将熵和检验位组合起来
bits = entropyBits + checksumBits

// 将最终的熵以11位为一组分割开来
const chunks = bits.match(/(.{1,11})/g);

console.log(chunks)


// 从 bit39 中获取一个 wordlist 单词列表
wordlists = bip39.wordlists['EN']
// 熵转化的数字转换成单词,将单词连接起来生成助记词
const words = chunks.map(binary => {
    const index = binaryToByte(binary);
    return wordlists[index];
});

mnemonic = words.join(" ")
console.log(mnemonic)



// 生成检验位的函数
function deriveChecksumBits(entropyBuffer) {
	// entropyBuffer是字节,所以它的二进制长度要乘以八
    const ENT = entropyBuffer.length * 8;
    // CS 的长度
    const CS = ENT / 32;
    // 将熵进行 SHA-256 哈希
    const hash = createHash('sha256')
        .update(entropyBuffer)
        .digest();
    // 将hash的字节转化成二进制,然后截取前CS位的二进制进行返回
    return bytesToBinary([...hash]).slice(0, CS);
}


// 字节转化成二进制的函数
function bytesToBinary(bytes) {
    return bytes.map(x => lpad(x.toString(2), '0', 8)).join('');
}


// 将二进制转化为十进制字节
function binaryToByte(bin) {
    return parseInt(bin, 2);
}


function lpad(str, padString, length) {
    while (str.length < length)
        str = padString + str;
    return str;
}


/* 输出结果
11111010100000101100100010111011001011111101000000101111100000101111111100000001
000101000000011010011111001011100101101000011001
1010
[
  '11111010100', '00010110010',
  '00101110110', '01011111101',
  '00000010111', '11000001011',
  '11111100000', '00100010100',
  '00000110100', '11111001011',
  '10010110100', '00110011010'
]
whisper bind conduct garden actual scrap winter card alley west notable crew
*/

助记词生成种子

现在我们获得了助记词,我们需要将他转化成最终的种子

助记词表示长度为128至256位的熵。 通过使用密钥延伸函数PBKDF2,熵就可以导出成较长的(512位)种子。

PBKDF2的基本原理是通过一个伪随机函数(例如HMAC函数),把明文和一个盐值作为输入参数,然后重复进行运算,并最终产生密钥。如果重复的次数足够大,破解的成本就会变得很高。而盐值的添加也会增加“彩虹表”攻击的难度。

盐值可以表示密码(也可以不写),助记词 + 密码 通过 PBKDF2 函数生成(512位)种子,同时可以让我们使用相同的助记词生成不同的种子。


const bip39 = require('bip39');
let mnemonic = 'whisper bind conduct garden actual scrap winter card alley west notable crew'
let password = '1234asd'
let seed = bip39.mnemonicToSeedSync(mnemonic,password).toString('hex')

console.log(seed)


/* 输出结果
4376cef2cd74bee8d3b61a8eb42dd79efcac4920b456a017c8f524fbd76f689501c4f31f1f893fa7
6fb8fdbec7ef5da014772d80549184e1e52f30fc2f17eb32
*/

总结

补充

当然 nodeJs 中的 bip39 包给了我们更加方便的方法,我们无需生成大随机数(熵),就可以直接生成助记词,然后通过助记词和密码生成种子。

const bip39 = require('bip39');
let mnemonic = bip39.generateMnemonic(128)
let seed = bip39.mnemonicToSeedSync(mnemonic,password).toString('hex')
console.log(seed)

END