美文网首页
Bitcoin私钥、公钥、地址概念以及Java实现

Bitcoin私钥、公钥、地址概念以及Java实现

作者: Daemon_Shell | 来源:发表于2018-11-23 16:44 被阅读0次


        这几个月一直在忙自己的终身大事,好久不写文章了;静下来分析写作中断的原因,发现一半是工作太忙,一半是自己太懒,坚持很多年做一件事情真得挺难的。从今天开始,我要拾起写作计划,写作可以梳理自己的知识框架,同时分享给大家一起学习,欢迎大家提出建议。

        之前的写作内容都是区块链入门级别的,出于个人爱好,主要围绕ethereum展开,但是工作中基本不涉及ethereum的内容,一直没有深入下去;由于工作围绕BitCoin展开,以后的文章我先转移到BTC领域。

        今天先从BTC的基本概念入手,整理一下私钥、公钥、地址的概念,这些概念在网络上已经泛滥很久了,实在是太枯燥,还是自己coding来得爽,今天就和大家分享如何用Java生成BTC私钥、公钥和地址。理论方面先分享几个链接:

        https://en.bitcoin.it/wiki/Private_key

        比特币密钥生成规则及 Go 实现 (特别推荐,这篇文章质量很高)

        这里强调一下理论的重要性,只有彻底理解了BTC私钥、公钥、地址的关系,才能coding出来,所有的代码只是思想的体现。

    Java 代码实现

     代码部分只贴出了核心片段,稍微加工一下即可运行。

    // 主要使用了java lang 有关椭圆曲线算法的package, bouncycastle lib(bcprov-jdk15on-160.jar)以及bitcoinj的Base58类

    import java.security.KeyPair;

    import java.security.KeyPairGenerator;

    import java.security.spec.ECGenParameterSpec;

    import java.security.spec.ECPoint;

    import java.security.PublicKey;

    import java.security.PrivateKey;

    import java.security.interfaces.ECPrivateKey;

    import java.security.interfaces.ECPublicKey;

    import java.security.InvalidAlgorithmParameterException;

    import java.security.NoSuchAlgorithmException;

    import java.security.MessageDigest;

    import java.security.NoSuchProviderException;

    import java.security.Security;

    import java.io.UnsupportedEncodingException;

    import org.bouncycastle.jce.provider.BouncyCastleProvider;

    import org.bitcoinj.core.Base58;

    // 定义Key类,用来封装公私钥对和地址

    class Key {

        private String privkey;

        private String pubkey;

        private String address;

        public Key() {

            Reset();

        }

        public Key(String privkey, String pubkey, String address) {

            this.privkey = privkey;

            this.pubkey  = pubkey;

            this.address = address;

        }

        public void Reset() {

            this.privkey = null;

            this.pubkey  = null;

            this.address = null;

        }

        public void SetPrivKey(String privkey) {

            this.privkey = privkey;

        }

        public void SetPubKey(String pubkey) {

            this.pubkey = pubkey;

        }

        public void SetAddress(String address) {

            this.address = address;

        }

        public String ToString() {

            return "{\n"

                + "\t privkey:" + this.privkey + "\n"

                + "\t pubkey :" + this.pubkey  + "\n"

                + "\t address:" + this.address + "\n"

                + "}\n";

        }

    }

    public class KeyGenerator {

       // Base58 encode prefix,不同的prefix可以定制地址的首字母

        static final byte PubKeyPrefix = 65;

        static final byte PrivKeyPrefix = -128;

        static final String PrivKeyPrefixStr = "80";

        static final byte PrivKeySuffix = 0x01;

        static int keyGeneratedCount = 1;

        static boolean debug = true;

        static KeyPairGenerator sKeyGen;

        static ECGenParameterSpec sEcSpec;

        static {

                Security.addProvider(new BouncyCastleProvider());

        }

        private static boolean ParseArguments(String []argv) {

            for (int i = 0; i < argv.length - 1; i++) {

                if ("-n".equals(argv[i])) {

                    try {

                        keyGeneratedCount = Integer.parseInt(argv[i + 1]);

                        i = i + 1;

                        continue;

                    } catch (NumberFormatException e) {

                        e.printStackTrace();

                        return false;

                    }

                } else if ("-debug".equals(argv[i])) {

                    debug = true;

                } else {

                    System.out.println(argv[i] + " not supported...");

                    return false;

                }

            }

            return keyGeneratedCount > 0;

        }

        public static void main(String args[]) {

            if (args.length > 1) {

              if (!ParseArguments(args)) {

                  System.out.println("Arguments error, please check...");

                  System.exit(-1);

              }

            }

            Key key = new Key();

            key.Reset();

            KeyGenerator generator = new KeyGenerator();

            for (int i = 0; i < keyGeneratedCount; i++) {

                key.Reset();

                if (generator.GenerateKey(key)) {

                    System.out.println(key.ToString());

                } else {

                    System.out.println("Generate key error...");

                    System.exit(-1);

                }

            }

        }

        public KeyGenerator() {

            Init();

        }

        private void Init() {

            // Initialize key generator

            // The specific elliptic curve used is the secp256k1.

            try {

                sKeyGen = KeyPairGenerator.getInstance("EC");

                sEcSpec = new ECGenParameterSpec("secp256k1");

                if (sKeyGen == null) {

                    System.out.println("Error: no ec algorithm");

                    System.exit(-1);

                }

                sKeyGen.initialize(sEcSpec); // 采用secp256K1标准的椭圆曲线加密算法

            } catch (InvalidAlgorithmParameterException e) {

                System.out.println("Error:" + e);

                System.exit(-1);

            } catch (NoSuchAlgorithmException e) {

                System.out.println("Error:" + e);

                System.exit(-1);

            } catch (Exception e) {

                System.out.println("Error:" + e);

                System.exit(-1);

            }

        }

        public boolean GenerateKey(Key key) {

            key.Reset();

            // Generate key pair,依据椭圆曲线算法产生公私钥对

            KeyPair kp = sKeyGen.generateKeyPair();

            PublicKey pub = kp.getPublic();

            PrivateKey pvt = kp.getPrivate();

            ECPrivateKey epvt = (ECPrivateKey)pvt;

            String sepvt = Utils.AdjustTo64(epvt.getS().toString(16)).toUpperCase(); // 私钥16进制字符串

            if (debug) {

                System.out.println("Privkey[" + sepvt.length() + "]: " + sepvt);

            }

          // 获取X,Y坐标点,“04” + sx + sy即可获得完整的公钥,但是这里我们需要压缩的公钥

            ECPublicKey epub = (ECPublicKey)pub;

            ECPoint pt = epub.getW();

            String sx = Utils.AdjustTo64(pt.getAffineX().toString(16)).toUpperCase();

            String sy = Utils.AdjustTo64(pt.getAffineY().toString(16)).toUpperCase();

            String bcPub = "04" + sx + sy;

            if (debug) {

                System.out.println("Pubkey[" + bcPub.length() + "]: " + bcPub);

            }

            // Here we get compressed pubkey

           // 获取压缩公钥的方法:Y坐标最后一个字节是偶数,则 "02" + sx,否则 "03" + sx

            byte[] by = Utils.HexStringToByteArray(sy);

            byte lastByte = by[by.length - 1];

            String compressedPk;

            if ((int)(lastByte) % 2 == 0) {

                compressedPk = "02" + sx;

            } else {

                compressedPk = "03" + sx;

            }

            if (debug) {

                System.out.println("compressed pubkey: " + compressedPk);

            }

            key.SetPubKey(compressedPk);

            // We now need to perform a SHA-256 digest on the public key,

            // followed by a RIPEMD-160 digest.

           // 对压缩的公钥做SHA256摘要

            byte[] s1 = null;

            MessageDigest sha = null;

            try {

                sha = MessageDigest.getInstance("SHA-256");

                s1 = sha.digest(Utils.HexStringToByteArray(compressedPk));

                if (debug) {

                    System.out.println("sha: " + Utils.BytesToHex(s1).toUpperCase());

                }

            } catch (NoSuchAlgorithmException e) {

                System.out.println("Error:" + e);

                return false;

            }

            // We use the Bouncy Castle provider for performing the RIPEMD-160 digest

            // since JCE does not implement this algorithm.

            // SHA256摘要之后做RIPEMD-160,这里调用Bouncy Castle的库,不知道的同学百度搜一下就懂了

            byte[] r1 = null;

            byte[] r2 = null;

            try {

                MessageDigest rmd = MessageDigest.getInstance("RipeMD160", "BC");

                if (rmd == null || s1 == null) {

                    System.out.println("can't get ripemd160 or sha result is null");

                    return false;

                }

                r1 = rmd.digest(s1);

                r2 = new byte[r1.length + 1];

                r2[0] = PubKeyPrefix; // RipeMD160 摘要之后加上公钥前缀

                for (int i = 0; i < r1.length; i++)

                    r2[i + 1] = r1[i]; // 写的有点low,大家采用System.arraycopy自行修改吧

                if (debug) {

                    System.out.println("rmd: " + Utils.BytesToHex(r2).toUpperCase());

                }

            } catch (NoSuchAlgorithmException e) {

                System.out.println("Error:" + e);

                return false;

            } catch (NoSuchProviderException e) {

                System.out.println("Error:" + e);

                return false;

            }

            byte[] s2 = null; // 加上前缀之后做两次SHA256

            if (sha != null && r2 != null) {

                sha.reset();

                s2 = sha.digest(r2);

                if (debug) {

                    System.out.println("sha: " + Utils.BytesToHex(s2).toUpperCase());

                }

            } else {

                System.out.println("cant't do sha-256 after ripemd160");

                return false;

            }

            byte[] s3 = null;

            if (sha != null && s2 != null) {

                sha.reset();

                s3 = sha.digest(s2);

                if (debug) {

                    System.out.println("sha: " + Utils.BytesToHex(s3).toUpperCase());

                }

            } else {

                System.out.println("cant't do sha-256 after sha-256");

                return false;

            }

            // 读懂下面内容,大家仔细阅读比特币密钥生成规则及 Go 实现

            byte[] a1 = new byte[r2.length + 4];

            for (int i = 0 ; i < r2.length ; i++) a1[i] = r2[i];

            for (int i = 0 ; i < 4 ; i++) a1[r2.length + i] = s3[i];

            if (debug) {

                System.out.println("before base58: " + Utils.BytesToHex(a1).toUpperCase());

            }

            key.SetAddress(Base58.encode(a1)); // 到此,可以获取WIF格式的地址

            if (debug) {

                System.out.println("addr: " + Base58.encode(a1));

            }

            // Lastly, we get compressed privkey 最后获取压缩的私钥

            byte[] pkBytes = null;

            pkBytes = Utils.HexStringToByteArray("80" + sepvt + "01");//sepvt.getBytes("UTF-8");

            if (debug) {

                    System.out.println("raw compressed privkey: " + Utils.BytesToHex(pkBytes).toUpperCase());

                }

            try {

                sha = MessageDigest.getInstance("SHA-256");

            } catch (NoSuchAlgorithmException e) {

                System.out.println("Error:" + e);

                return false;

            }

            sha.reset();

            byte[] shafirst  = sha.digest(pkBytes);

            sha.reset();

            byte[] shasecond = sha.digest(shafirst);

            byte[] compressedPrivKey = new byte[pkBytes.length + 4];

            for (int i = 0; i < pkBytes.length; i++) {

                compressedPrivKey[i] = pkBytes[i];

            }

            for (int j = 0; j < 4; j++) {

                compressedPrivKey[j + pkBytes.length] = shasecond[j];

            }

            //compressedPrivKey[compressedPrivKey.length - 1] = PrivKeySuffix;

            key.SetPrivKey(Base58.encode(compressedPrivKey));

            if (debug) {

                System.out.println("compressed private key: " + Base58.encode(compressedPrivKey));

            }

            return true;

        }

    }

    // 附上Utils中的静态方法,都很简单

    public class Utils {

        public static String AdjustTo64(String s) {

            switch(s.length()) {

                case 62: return "00" + s;

                case 63: return "0" + s;

                case 64: return s;

                default:

                    throw new IllegalArgumentException("not a valid key: " + s);

            }

        }

        public static String BytesToHex(byte[] src) {

            StringBuilder stringBuilder = new StringBuilder("");

            if (src == null || src.length <= 0) {

                return null;

            }

            for (int i = 0; i < src.length; i++) {

                int v = src[i] & 0xFF;

                String hv = Integer.toHexString(v);

                if (hv.length() < 2) {

                    stringBuilder.append(0);

                }

                stringBuilder.append(hv);

            }

            return stringBuilder.toString();

        }

        public static byte[] HexStringToByteArray(String s) {

            int len = s.length();

            byte[] data = new byte[len / 2];

            for (int i = 0; i < len; i += 2) {

                data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)

                    + Character.digit(s.charAt(i+1), 16));

            }

            return data;

        }

    }

    总结

        如果以上代码仍有疑问,请仔细阅读上面推荐的两篇文章,请时刻记住:code只是理论的体现。

    这里总结一下BitCoin 生成私钥、压缩格式私钥、公钥、压缩格式公钥、WIF钱包地址的过程:

    a. secp256K1标准的EC算法生成公私钥对:(privkey, pubkey);

    b. privkey 生成压缩格式私钥:假设privkey1 = 私钥前缀0x80+privkey+私钥后缀x01,

        result1 = sha256(sha256(privkey1)),对 privkey1做两次SHA256摘要, result1前4个字节添加到privkey1, privkey2 = privkey1 + result1[0:3],压缩私钥compPrivkey = base58(privkey2);

    c. pubkey 生成完整的公钥和压缩格式的公钥:pubkey对应一个坐标点(X,Y),由X可以推算出Y,

        0x04 + X + Y就是完整的公钥;设Y的最后一个字节为b,则:

        b为偶数,压缩格式的公钥compPubkey = 0x02 + x,

        b为奇数,压缩格式的公钥compPubkey = 0x03 + x。

    d. 压缩的公钥compPubkey生成WIF格式的地址address:

        假设 r1 =  RIPEMD160(SHA256(compPubkey)),压缩公钥先做SHA256,在做RIPEMD160摘要;

        假设 r2 = PubkeyPrefix(这里为10进制65) + r1;

        假设 s3 = SHA256(SHA256(r2)),r2两次SHA256摘要,s3的前4个字节为s3[0:3];

        假设 a = r2 + s3[0:3],WIF address = base58(a)。

        以上就是简单的总结,比较繁琐,至于为什么这么做,那是bitcoin设计师设计的,请大家查看官方资料。

        最后留给大家一个问题:compressed privkey如何得到完整的私钥匙??别看补充内容,自己先想想!

    补充(压缩私钥匙转为完整私钥):

    神奇的事情是这样发生的,对compressed privkey做base58 decode,结果为38个字节,结构为:

    1字节前缀(0x80) + 32 字节私钥 + 1字节后缀(0x01) + 4 字节(这四个字节就是上面result1头4个字节)。

    神奇吧?So amazing! 最后送给小伙伴们压缩私钥转换为原始私钥的code:

    public static String convertWIFPrivkeyIntoPrivkey(String wifPrivKey) throws AddressFormatException {

            if (wifPrivKey == null || "".equals(wifPrivKey)) {

                throw new AddressFormatException("Invalid WIF private key");

            }

            byte[] base58Decode = null;

            try {

                base58Decode = Base58.decode(wifPrivKey);

            } catch (AddressFormatException e) {

                throw e;

            }

            String decodeStr = Utils.bytesToHexString(base58Decode);

            if (decodeStr.length() != 76) {

                throw new AddressFormatException("Invalid WIF private key");

            }

            String version = decodeStr.substring(0, 2);

            String suffix = decodeStr.substring(66, 68);

            if (!"80".equals(version) || !"01".equals(suffix)) {

                throw new AddressFormatException("Invalid WIF private key");

            }

            String privKeyStr = decodeStr.substring(2, 66);

            return privKeyStr;

        }

    相关文章

      网友评论

          本文标题:Bitcoin私钥、公钥、地址概念以及Java实现

          本文链接:https://www.haomeiwen.com/subject/zjpoxqtx.html