开源库

本文中用到的加密算法实现开源项目如下:

java:https://github.com/ZZMarquis/gmhelper

golang:https://github.com/Hyperledger-TWGC/ccs-gm

js:https://github.com/JuneAndGreen/sm-crypto

SM2

SM2的调试较为复杂,需要注意的点有两个:1、密钥对的生成;2、JS加解密时的特殊处理。

生成密钥对

经过测试,只有golang生成的密钥对才能在golang、java和js三端通用,生成方式如下:

iniSk, _ := sm2.GenerateKey(rand.Reader)
iniPk := iniSk.PublicKey

pemSk, err := PrivateKeyToPEM(iniSk, nil)
if err != nil {
t.Errorf("private key to pem error %t", err)
}
fmt.Printf("pem私钥:%s", string(pemSk))

pemPk, err := PublicKeyToPEM(&iniPk, nil)
if err != nil {
t.Errorf("public key to pem error %t", err)
}
fmt.Printf("pem公钥:%s", string(pemPk))

生成的PEM密钥可以直接在golang和java中使用,但是js的密钥还需要做一次转换,这里使用Java做转换:

String privatePem = "这里填上文生成的pem私钥";
byte[] pkcs8PrivateKey = BCECUtil.convertECPrivateKeyPEMToPKCS8(privatePem);
System.out.println(HexUtil.getHexString(pkcs8PrivateKey));

BCECPrivateKey ecpPrivateKey = BCECUtil.convertPKCS8ToECPrivateKey(pkcs8PrivateKey);
ECPrivateKeyParameters privateKeyParameters = BCECUtil.convertPrivateKeyToParameters(ecpPrivateKey);
System.out.println("JS私钥: " + HexUtil.byteToHex(privateKeyParameters.getD().toByteArray()));

String publicPem = "这里填上文生成的pem公钥";
byte[] x509PublicKey = BCECUtil.convertECPublicKeyPEMToX509(publicPem);

String publicKey = HexUtil.getHexString(x509PublicKey);
System.out.println(publicKey);

BCECPublicKey bcecPublicKey = BCECUtil.convertX509ToECPublicKey(x509PublicKey);
ECPublicKeyParameters publicKeyParameter = BCECUtil.convertPublicKeyToParameters(bcecPublicKey);
System.out.println("JS公钥: " + HexUtil.byteToHex(publicKeyParameter.getQ().getEncoded(false)));

java的算法实现中没有给出 HexUtil的实现,这里贴出我这里的实现:

HexUtil

golang加解密

// 测试sm2的加密和解密
func TestEncryptAndDecrypt(t *testing.T) {
pempk := `这里填上文生成的pem公钥`
normalPk, err := utils.PEMtoPublicKey([]byte(pempk), nil)
if err != nil {
fmt.Println(err)
}

pemSk := `这里填上文生成的pem私钥`
normalSk, err := utils.PEMtoPrivateKey([]byte(pemSk), nil)
if err != nil {
fmt.Println(err)
}

msg := []byte("helloword123123!")
//test encryption
cipher, err := sm2.Encrypt(rand.Reader, normalPk, msg)
fmt.Println("===================================================")
fmt.Println("加密后字符串:", hex.EncodeToString(cipher))

fmt.Println("===================================================")
res, err := sm2.Decrypt(cipher, normalSk)
if err != nil {
fmt.Println("解密失败:", err)
}
fmt.Println("解密后字符串:", string(res))
}

java加解密

String publicPem = "这里填上文生成的pem公钥";
byte[] x509PublicKey = BCECUtil.convertECPublicKeyPEMToX509(publicPem);
BCECPublicKey publicKey = BCECUtil.convertX509ToECPublicKey(x509PublicKey);
byte[] cipherText = SM2Util.encrypt(publicKey, PLAIN_TEXT.getBytes(StandardCharsets.UTF_8));
System.out.println("加密后密文:"+HexUtil.getHexString(cipherText));

String privatePem = "这里填上文生成的pem私钥";
byte[] pkcs8PrivateKey = BCECUtil.convertECPrivateKeyPEMToPKCS8(privatePem);
BCECPrivateKey privateKey = BCECUtil.convertPKCS8ToECPrivateKey(pkcs8PrivateKey);
byte[] plainText = SM2Util.decrypt(privateKey, HexUtil.hexToByte("这里填加密后密文"));
System.out.println("解密后明文:"+new String(plainText));

js加解密

注意:

JS生成的密文必须加前缀“04”才能被golang和java正确解密

golang和java生成的密文一定要去除前缀“04”,再将密文全部转为小写字母后方可正确解密

const sm2 = require('sm-crypto').sm2

encryptData = sm2.doEncrypt(msgString, publicKey, cipherMode)
console.log('加密后密文:', encryptData)
// 这里需要注意,js生成的密文需要加前缀"04"后才能被golang和java正确解密

// 这里需要注意,golang和java生成的密文一定要去除前缀“04”,再将密文全部转为小写字母后方可正确解密
encryptData = '加密后密文'
decryptData = sm2.doDecrypt(encryptData, privateKey, cipherMode)
console.log('解密后明文', decryptData)

SM3

SM3的调试比较简单,下面只列出各个语言的调用方法。

每种语言生成的密文会有大小写的差异,实际使用时可以忽略大小写进行对比。

golang加密

func TestSM3HashWithJava(t *testing.T) {
hash := sm3.New()
hash.Write([]byte("helloword!sm3"))
hashed := hash.Sum(nil)
fmt.Println("SM3加密密文: ", hex.EncodeToString(hashed))
}

java加密

@Test
void testHash() {
byte[] hashed = SM3Util.hash(PLAIN_TEXT.getBytes(StandardCharsets.UTF_8));
System.out.println(new String(hashed));
System.out.println(HexUtil.getHexString(hashed));
SM3Util.verify(PLAIN_TEXT.getBytes(StandardCharsets.UTF_8), hashed);
}

js加密

msgString = 'helloword!sm3'
const sm3 = require('sm-crypto').sm3
hashData = sm3(msgString) // 杂凑hashData
console.log('【hashData】', hashData)

SM4

SM4的调试也比较简单,只需要注意key的生成和js加密key的特殊处理

golang加解密

func TestSM4(t *testing.T) {
key := []byte("0123456789abcdef")
msg := []byte("0123456789abcdef012345678")
encMsg, err := sm4.Sm4Ecb(key, msg, sm4.ENC)
if err != nil {
t.Errorf("sm4 enc error:%s", err)
return
}
fmt.Println("加密后的密文:", hex.EncodeToString(encMsg))

encMsg, err = hex.DecodeString("13278C63B7305E3C131CDBBBE6F59B86A6B8937FE1E2B336779FE4DB0537E750")
dec, err := sm4.Sm4Ecb(key, encMsg, sm4.DEC)
if err != nil {
t.Errorf("sm4 dec error:%s", err)
return
}
fmt.Println("解密后的明文:", string(dec))

if !bytes.Equal(msg, dec) {
t.Errorf("sm4 self enc and dec failed")
}
}

java加解密

@Test
public void testECBPadding() throws Exception {
byte[] key = "0123456789abcdef".getBytes(StandardCharsets.UTF_8);

// 这里需要注意,16进制加密key用于js端
System.out.println("16进制加密key: " + HexUtil.byteToHex(key));
System.out.println("加密key: " + new String(key));
byte[] data = "0123456789abcdef012345678".getBytes(StandardCharsets.UTF_8);

byte[] cipherText = SM4Util.encrypt_ECB_Padding(key, data);
System.out.println("SM4 ECB Padding encrypt result: " + HexUtil.byteToHex(cipherText));

byte[] decryptedData = SM4Util.decrypt_ECB_Padding(key, cipherText);
System.out.println("SM4 ECB Padding decrypt result: " + new String(decryptedData));
System.out.println(new String(decryptedData));
if (!Arrays.equals(decryptedData, data)) {
System.out.println("加解密失败");
}
}

js加解密

const sm4 = require('sm-crypto').sm4

// 这里需要注意,一定要使用上文java测试类中生成的16进制加密key
key = '30313233343536373839616263646566'
msgString = '0123456789abcdef012345678'

encryptData = sm4.encrypt(msgString, key) // 加密,默认输出 16 进制字符串,默认使用 pkcs#5 填充
console.log('【encryptData】', encryptData)
decryptData = sm4.decrypt(encryptData, key) // 解密,默认输出 utf8 字符串,默认使用 pkcs#5 填充
console.log('【decryptData】', decryptData)