Bitcoin私钥、公钥、地址概念以及Java实现
序
这几个月一直在忙自己的终身大事,好久不写文章了;静下来分析写作中断的原因,发现一半是工作太忙,一半是自己太懒,坚持很多年做一件事情真得挺难的。从今天开始,我要拾起写作计划,写作可以梳理自己的知识框架,同时分享给大家一起学习,欢迎大家提出建议。
之前的写作内容都是区块链入门级别的,出于个人爱好,主要围绕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;
}