V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
zy445566
V2EX  ›  Node.js

Bitcoin 公私钥是如何生成的

  •  
  •   zy445566 · 2019-05-29 14:10:22 +08:00 · 6043 次点击
    这是一个创建于 2038 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原本一直都是靠比特币钱包生成公私钥的,但感觉一直不是很放心,尤其是 npm 包隐藏盗币代码后,一直感觉危险重重,加上货币交易所,也是存在倒闭或跑路的风险,毕竟是第三方。加之又看到 node12 支持了原生的 BigInt,想着是时候自己做个无第三方依赖的公私钥生成工具了。

    先谈私钥生成

    私钥是如何产生的呢?简单来说就是在一个大数中选值,最后进行按照一些规则加密成我们所使用的私钥。我这边使用了两种方法实现,一个是随机法,一个是加密法生成。

    先看加密生成法

    // 简易 sha256 通过字符串生成摘要
    function getPrivteOriginKeyByStr(strSeed) {
        return crypto.createHash('sha256').update(strSeed).digest('hex');
    }
    

    这里就是直接使用 sha256 生成摘要,然后生成一个十六进制的私钥原值

    再随机生成法

    const n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141n;
    function getPrivteOriginKeyByRand() {
        let nHex = n.toString(16);
        let privteKeyList = [];
        let isZero = true;
        for(let i=0;i<nHex.length;i++) {
            let rand16Num = Math.round(Math.random()*parseInt(nHex[i],16));
            privteKeyList.push(rand16Num.toString(16));
            if(rand16Num>0) {isZero = false;}
        }
        if(isZero){getPrivteOriginKeyByRand();}
        return privteKeyList.join('');
    }
    

    这里就是通过通过每位进行随机,再组合生成一个长串,目前 node 的随机数种子在 linux 中会取一个文件的指纹(这个文件是会不断变化的,以前看过代码,现在有点忘记叫什么文件了),所以你不用当心第一次生成都会是一样的值。

    第二步就是将原值转换成我们能导入钱包的私钥

    转换规则是原值加上版本做前缀,进行两次 sha256 加密,同时取前 4 个字节,进行 58 进制转换。如下。

    function hex2sha256(hexStr) {
        return crypto.createHash('sha256').update(Buffer.alloc(hexStr.length/2,hexStr, 'hex')).digest('hex');
    }
    
    function getPrivteKeyByOrigin(privteKeyOrigin) {
        if(privteKeyOrigin.length!==64){
            throw new Error('privte Key Origin length must be 64!')
        }
        let version = '80';
        let sha1Str = `${version}${privteKeyOrigin}`;
        let sha1 =  hex2sha256(sha1Str);
        let sha2Str = `${sha1}`;
        let sha2 =  hex2sha256(sha2Str);
        let key = `${version}${privteKeyOrigin}${sha2.slice(0,8)}`;
        return util.hex2Base58(key);
    }
    

    这里你可能会有几个疑问:

    • 为什么 hexStr.length 要除以 2 ?
      • 因为一个字节是 255,两个十六进制数才构成一个字节,所以需要除 2,同时如直接使用文本则会直接已默认 utf8 的对象传入一个十六进制字符占一字节导致计算不准确。
    • 那之前说好前 4 个字节,为什么要 sha2.slice(0,8)?
      • 和上面一样 sha2 是 16 进制数,4 字节就是十六进制数的 8 位。
    • 58 进制是什么鬼?
      • 58 进制其实就是数字和大小写字母中剔除了大写 i,大写 o,小写 L,数字 0,因为这些无论是被当作地址还是作为私钥都容易被人混淆,所以去除了。数字加大小写共 62 位,减去 4 位则为 58 位。

    再谈公钥生成

    第一步公钥的原值

    公钥原值生成其实是采用了椭圆加密算法,简单来说就是使用了 E : y^2 ≡ x^3 + ax + b (mod p)算法实现椭圆曲线,然后使用 K=kG,计算公钥,小 k 是私钥,大 K 我们要求的公钥,G 是椭圆曲线上的一个点,这是一个常量。

    注意的点是 kG 并不是代表 k 和 G 点的乘基,而是又前一个点推导到后一个点。

    可以用如下公司求解(其中 a,p 都是常量):

    相同的点相加第一式: λ≡(3x1^2+ a)/2y1(mod p)
    相同的点相加第二式: x3 ≡ λ^2 − 2x1 (mod p), y3≡ λ(x1 − x3) − y1 (mod p)
    
    不同的点相加第一式: λ≡ ( y2 − y1 )/( x2 − x1 )(mod p)
    不同的点相加第二式: x3 ≡ λ^2 − x1 − x2 (mod p), y3 ≡ λ(x1 − x3) − y1 (mod p)
    

    第一步:比如 G 点的 x,y 坐标是 x1,y1,那么这时我需要求解 2G,那么先用 G 导入“相同的点相加第一式”,求出λ,然后“相同的点相加第二式”求解 x3,y3,这个点就是 2G 了。

    第二步:现在有 G 和 2G 两个点,那么 3G 的求解则是将 2G 带入“不同的点相加第一式”去减 G,求出λ,然后再用“不同的点相加第二式”求解 x3,y3,这个点就是 3G 了。

    然后 4G,5G,6G...kG 可以不断使用第二步循环执行来得出。

    目前本人正在写这部分的原生纯算法实现,但是目前生成公钥因为不需要签名所以,我直接用了 node 的 ECDH 库,因为 ECDH 和 ECDSA 仅仅是椭圆加密算法的不同实现,所以生成公钥可以直接使用。如下。

    function getPublicOriginKey(privteKeyOrigin) {
        if(privteKeyOrigin.length!==64){
            throw new Error('privte Key Origin length must be 64!')
        }
        const ecdh = crypto.createECDH('secp256k1');
        ecdh.setPrivateKey(privteKeyOrigin,'hex');
        return ecdh.getPublicKey('hex');
    }
    

    第二步公钥原值生成公钥地址

    这里其实就是将公钥原值,先进行一次 hex2sha256 运算,然后使用再 ripemd160 加密上一步结果,将结果增加主网号 00 后再进行两次加密,取 ripemd160 加密结果和上一次结果的前四个字节,组成 key 走一个转 58 进制,加地址标示 1,生成公钥地址。如下。

    function getPublicKeyByOrigin(publicKeyOrigin) {
        let mainVersionHex = '00';
        let addreeSign = '1';
        let sha1 = hex2sha256(publicKeyOrigin);
        let ripemd160Hex =  crypto.createHash('ripemd160').update(Buffer.alloc(sha1.length/2,sha1, 'hex')).digest('hex');
        let ripemd160HexUsed =`${mainVersionHex}${ripemd160Hex}`;
        let sha2 =  hex2sha256(ripemd160HexUsed);
        let sha3 =  hex2sha256(sha2);
        let key = `${ripemd160HexUsed}${sha3.slice(0,8)}`;
        return `${addreeSign}${util.hex2Base58(key)}`;
    }
    

    生成公钥地址可以说是不可逆的,首先用椭圆加密算法将私钥进行了数学难题加密,再通过摘要算法,只获取摘要信息,所以简单来说这个公钥地址也仅仅是原值的摘要而已,连要复原公钥原值都不太可能。

    附文

    欢迎大家使用 bitcoin-key-generator(代码点此,感谢点星)来生成私钥,代码不多,无第三方库,可以看后再生成保证自己私钥安全。最后希望大家能推荐我一个杭州仓前不加班 955 的坑,工资可谈。

    13 条回复    2019-05-31 14:56:02 +08:00
    niubee1
        1
    niubee1  
       2019-05-29 14:44:46 +08:00
    你自己的钱包再怎么安全,交易还不是得去交易所。
    uqin
        2
    uqin  
       2019-05-29 14:51:49 +08:00 via iPad
    @niubee1 也可以 otc,不一定要去交易所
    dingyaguang117
        3
    dingyaguang117  
       2019-05-29 15:04:36 +08:00
    @niubee1 还有 DEX
    leishi1313
        4
    leishi1313  
       2019-05-30 06:57:27 +08:00
    直接调 Math.random()生成的随机数都不靠谱,更别说直接对 str sha256 了,安全性连脑钱包都不如。有兴趣可以研究下 https://www.bitaddress.org/的代码,人家也是开源的。但大额的钱包还是不敢用任何网站生成的,充其量也就是生成个 onchain 的测试地址,真要存自己钱还是得硬件钱包
    zy445566
        5
    zy445566  
    OP
       2019-05-30 08:41:28 +08:00 via Android
    @leishi1313 呵呵,只是个大数选择,你自己也可以随便写一个私钥原值。硬件钱包也是要根据私钥原值。这点无区别。
    我这边的安全是指无第三方 npm 依赖,代码简单一览无遗,不可能存在埋盗笔代码。
    zy445566
        6
    zy445566  
    OP
       2019-05-30 08:42:40 +08:00 via Android
    你要觉得安全,自己写个私钥原值就好。我这里可以根据原值生成私钥和地址。
    leishi1313
        7
    leishi1313  
       2019-05-30 13:20:03 +08:00 via Android
    @zy445566 不用动气,你既然放代码出来那想必是欢迎讨论的,我只是指出你提供的两种方法安全性(这里的安全指的是私钥不被人轻易猜出或者暴力破解)不足以真的存储比特币资产。你说的“安全”是所有的钱包生成器的标配,前面说过了 bitaddress.org 的代码基本从头到尾被无数人检验过了,理论上不会有暗门;硬件钱包比如 Ledger 也会有独立第三方检验钱包的安全性。至于说密钥只是一串 sha256,那理论上当然没错,256
    leishi1313
        8
    leishi1313  
       2019-05-30 13:25:33 +08:00 via Android
    @zy445566 但是随机性不够的密钥是很容易的被黑客盯上的,各种简单的原值 sha256 后的地址都有人随时盯着(我自己就写过类似的)。至于 random()并不真的随机这种也是计算机常识了。但是写写这些了解了解比特币私钥公钥还是挺好的,有兴趣也可以了解了解 bip39
    zy445566
        9
    zy445566  
    OP
       2019-05-30 13:53:36 +08:00
    @leishi1313
    我没有动气,是真的你可以自己写个私钥原值就好。64 位十六进制数在[1,n]范围内就好,我这里可以帮你加载成真正可以用的私钥。getPrivteOriginKeyByStr 和 getPrivteOriginKeyByRand 是完全可以不使用的,也是最没有技术的方法。

    至于 random()并不是对整串随机,而是对原值的十六进制数的每一位进行随机。也就是说 random()几乎是在随机 0 到 15.随机 64 次,而不是做一次随机就出串。这是有很大差别的,同时 node 是读取 /dev/urandom 来实现随机,并不是你想的读库随机,这点也是有很大的不同。

    一个私钥都是由私钥的原值产生,所以和硬件钱包没关系,我随机生成私钥原值就是故意不看 bitcoin-core 源码来实现,因为就是当心有人直接使用 bitcoin-core 源码来实现碰撞私钥,包括私钥通过私钥原值生成和公钥生成都参考了一定 bitcoin-core 源码来实现。

    你可能还不是很明白什么是私钥原值,所以你才会纠结随机问题,而这个原值是可以完全自己写一个自己喜欢的。
    leishi1313
        10
    leishi1313  
       2019-05-31 07:40:50 +08:00
    @zy445566 我说的就是这个“私钥原值”,我的观点一直以来都是你的“私钥原值”不够随机,很容易被人破解。你的私钥原值是什么,第一种方法是一串人类脑中的 string,第二种方法是你的 n 值加上系统的随机数种子(至于你说多次 random 更鲁棒什么的,这只能祈祷你 js 的 runtime 的实现了,可以搜搜“ Why Math.random should not be used for serious programming ”)。不管如何随机加密这些原值核心都只是一串 string 而已,这些都不够可靠。我前面提过的 bitaddress 的私钥原值是什么,是你鼠标在屏幕上的移动坐标,再加上 10 多种变化加密,硬件钱包的私钥原值是什么,除了厂家和检测机构没人知道,正是这样的随机性和不可复制性,才让这些可靠的秘钥生成方法足够鲁棒,不易破解。
    最后一个问题你说故意不去看 bitcoin-core 的实现所以你的秘钥生成机制才比 bitcoin-core 更安全,其实不是的,事实上 bitcoin-core 的秘钥生成比你的更加复杂,更加难碰撞,但是仍然有人试图碰撞,如果大家都采用你的方法来生成私钥,只会更加容易碰撞和破解。想要你的方法稍微安全点倒是有个方法,不要开源不要告诉别人你怎么加密你的私钥原值,这样别人可能还更云里雾里放弃破解
    zy445566
        11
    zy445566  
    OP
       2019-05-31 09:58:10 +08:00
    @leishi1313
    算了不知者无谓,给你讲一讲。
    ## 第一源码里的逻辑也不复杂,甚至和比我的还弱。
    首先在 src/bitcoin-wallet.cpp 里面执行 main 函数里的 RandomInit 方法执行初始化钱包随机。在 src/random.cpp 的 RandomInit 方法里默认执行 SeedFast 来生成原值。
    ```cpp
    static void SeedFast(CSHA512& hasher) noexcept
    {
    unsigned char buffer[32];
    const unsigned char* ptr = buffer;
    hasher.Write((const unsigned char*)&ptr, sizeof(ptr));
    SeedHardwareFast(hasher);

    SeedTimestamp(hasher);
    }
    ```
    在这个方法里就两个第一是随机生成原值再加个时间戳。加时间戳就不看了,看生成随机数。
    ```cpp
    static void SeedHardwareFast(CSHA512& hasher) noexcept {
    if (g_rdrand_supported) {
    uint64_t out = GetRdRand();
    hasher.Write((const unsigned char*)&out, sizeof(out));
    return;
    }
    }
    static uint64_t GetRdRand() noexcept
    {
    uint8_t ok;
    uint64_t r1;
    for (int i = 0; i < 10; ++i) {
    __asm__ volatile (".byte 0x48, 0x0f, 0xc7, 0xf0; setc %1" : "=a"(r1), "=q"(ok) :: "cc"); // rdrand %rax
    if (ok) break;
    }
    return r1;
    }
    ```
    这里我们只看比较新的架构生成,你可以看到这里直接调用了汇编的随机生成指令来生成。

    所以说官方就俩点
    * 用硬件生成了个随机码
    * 再加个时间戳

    就这样就给你了,`还真不如我的进行每位随机安全`。

    ## 第二黑客破解方法并不是通过私钥原值碰撞
    黑客拿到的是地址,所以
    * 首先要碰撞地址的 ripemd16 的值
    * 再通过 ripemd16 去碰撞一个 sha256 的值
    * 最后用 sha256 的值去反向减椭圆加密运算

    再所以,从头到尾跟私钥原值没关系,懂么?`只要私钥原值位数够长就行`。这就是我一直说私钥原值没关系的原因。
    zy445566
        12
    zy445566  
    OP
       2019-05-31 10:04:27 +08:00
    以上代码解析基于官方代码库: https://github.com/bitcoin/bitcoin
    leishi1313
        13
    leishi1313  
       2019-05-31 14:56:02 +08:00 via Android
    @zy445566 明白了,首先第一种方法就不谈了,针对你的第二种方法,所以你的观点是短的原值不够安全,只要随机次数足够多就能保证安全了是吧。我的观点是无论使用多少次 Math.random(),只要是 naive 地调用和简单变化都是不足以提供足够的安全性的,这点 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random 也有说。当然我不否认破解或者碰撞的难度,我所指的安全性是指数学上或者是 crypto 上的安全性,math.random 不足以提供足够的熵。但是你想比特币私钥有可能涉及到巨额的财产,没有谁会在这点上开玩笑。回过头来 bitcoin core wallet 和你的第二种方法提供的安全性用来日常小额交易存储应该都是可以的,也很方便,我自己其实有时候都会用 pycoin 和 sha256 直接命令行生成一个地址临时收发。但是冷钱包的秘钥肯定还是会用更鲁棒的方法来生成
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5562 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 03:30 · PVG 11:30 · LAX 19:30 · JFK 22:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.