关于国密对接遇到加密解密不兼容的问题

标题问题描述:

不同组件之间SM4加密解密不兼容

问题来源:

我校购买的一套“一卡通”系统中,使用的WebApi接口。

发现问题:

按照说明文档编写代码后,返回的数据无法解密。在C#之下,我尝试过BouncyCastle组件(用户数量超过4000万)、Sw.ChinaEncryptSM、Vive.Crypto。。。均无法解密。

研究问题:

跟该公司相关技术人员沟通交流、百度搜索、无尽地猜测、尝试。

问题回顾:

该公司提供的产品文档中,仅有如何加密的Java示例,但无解密的Java示例,且在百度上搜不到用于加密解密的Java类。公司的技术人员仅提供了一段go语言写的解密代码后,再无法提供其它的与C#相关资料。

go语言示例代码如下:

package main

import (
	"crypto/cipher"
	"encoding/hex"
	"fmt"

	"github.com/tjfoc/gmsm/sm4"
)

func sm4Dectypt(cipherText, key []byte) []byte {
	block, err := sm4.NewCipher(key)
	if err != nil {
		panic(err)
	}
	iv := []byte("-----------------------------------") 
	blockMode := cipher.NewCBCDecrypter(block, iv)
	blockMode.CryptBlocks(cipherText, cipherText)
	// plainText := unpaddingLastGroup(cipherText)
	return cipherText
}

func main() {
	key := []byte("-------------------")                                                             
	data1, _ := hex.DecodeString("----------------") 
	plainText := sm4Dectypt(data1, key)                                                              
	fmt.Printf("解密结果 = %s\n", plainText)
}

从"github.com/tjfoc/gmsm/sm4"可以看出,加密和解密用到github上一个项目。但github上此项目已经超过2年未更新。但,这段代码却可以解密服务器返回的数据。经过尝试,引用该项目也可完成加密。

经过对比,发现该项目中SM4加密的结果与C#中相关国密组件的加密结果不同,也就是该项目中的加密解密与其他标准组件存在兼容性问题

由于我校已经正式用该“一卡通”系统,因此,修改加密系统可能性不大,同时还会扯到费用的问题。因此,我们考虑将该github上的go代码移植到.NET平台,后来发现工作量巨大,遂放弃。从而考虑使用黑盒的方式使用该github项目,也就是将其编译成一个dll文件,然后在C#中调用。

将go代码编译成dll文件

由于需要不断尝试,因此,编写一个脚本文件来完成编译工作:

PATH=%PATH%;C:\TDM-GCC-64\bin
echo %PATH%
set cgo_enabled=1
go  build -a  -ldflags="-w -s"  -buildmode=c-shared -o SM3SM4GO.dll SM3SM4_CDUT_LIB.go
copy /y SM3SM4GO.dll C:\Users\Zmrbak\source\repos\ConsoleApp7\ConsoleApp7\

这里注意,需要安装gcc编译器。这里,我选择了“TDM-GCC-64”,相比来说安装简单方便,而且够用。
build:编译
-a :覆盖
-ldflags=“-w -s” :去除 DWARF调试信息、符号信息,以减小体积。
-buildmode=c-shared:构建模式,C共享方式
-o SM3SM4GO.dll:输出文件
SM3SM4_CDUT_LIB.go:go源文件

加密解密的坑

经过解密后,发现数据中有一个小尾巴(乱码,二进制0x80),反映给技术人员后,技术人员告知,是特意加上去的。
由于从服务器返回的数据解密后,是一个JSON字符串,却多了一个乱码小尾巴。本人直觉,觉得应该在Json反序列化前,得把数据处理干净,否则解析会出错(未验证),于是在解密代码中特意加了去掉小尾巴的逻辑。

后来才知道,加密的数据二进制位,必须是16的倍数,如果不足的话,需要用0补齐,该公司在末尾添加了一个0x80,以表示字符串结束,因此在解密的时候会有一个乱码小尾巴。(在我的记忆中,C字符串结束应该是0x00,也就是0,不知道这个公司为何用0x80)

编写go代码,封装加密解密

package main

import "C"
import (
	"crypto/cipher"
	"encoding/base64"
	"encoding/hex"
	"github.com/tjfoc/gmsm/sm3"
	"github.com/tjfoc/gmsm/sm4"
)

func main() {}

//export Sm4Decrypt
func Sm4Decrypt(cipherText *C.char, secretKey *C.char, secretIV *C.char, outParam **C.char, outline *C.int) bool {
	//将十六进制字符串转换为一个byte数组
	cipherBytes, _ := hex.DecodeString(C.GoString(cipherText))
	//将字符串转换为byte数组
	secretBytes := []byte(C.GoString(secretKey))
	ivBytes := []byte(C.GoString(secretIV))

	//解密
	block, err := sm4.NewCipher(secretBytes)
	if err != nil {
		panic(err)
	}
	blockMode := cipher.NewCBCDecrypter(block, ivBytes)
	blockMode.CryptBlocks(cipherBytes, cipherBytes)

	//将解密后的内容以Base64编码成一个字符串,然后返回
	res := base64.StdEncoding.EncodeToString(cipherBytes)
	*outParam = C.CString(res)
	*outline = C.int(len(C.GoString(*outParam)))
	return true
}

//export Sm4Encrypt
func Sm4Encrypt(plainText *C.char, secretKey *C.char, secretIV *C.char, outParam **C.char, outline *C.int) bool {
	//将字符串转换为byte数组
	textBytes := []byte(C.GoString(plainText))
	secretBytes := []byte(C.GoString(secretKey))
	ivBytes := []byte(C.GoString(secretIV))

	//16字节对齐
	len1 := len(textBytes) + 1
	len2 := len1
	if len2 <= 16 {
		len2 = 16
	}
	len3 := (16 - len2%16) + len2
	//固定长度数组
	buf := make([]byte, len3)

	//len1最初长度 len1-1
	for i := 0; i < len1-1; i++ {
		buf[i] = textBytes[i]
	}

	//末尾添加尾巴
	buf[len1-1] = 0x80

	//加密
	block, err := sm4.NewCipher(secretBytes)
	if err != nil {
		panic(err)
	}
	blockMode := cipher.NewCBCEncrypter(block, ivBytes)
	blockMode.CryptBlocks(buf, buf)

	//将加密后的内容以Base64编码成一个字符串,然后返回
	res := base64.StdEncoding.EncodeToString(buf)
	*outParam = C.CString(res)
	*outline = C.int(len(C.GoString(*outParam)))
	return true
}

//export Sm3Hash
func Sm3Hash(plainText *C.char, outParam **C.char, outline *C.int) bool {
	textBytes := []byte(C.GoString(plainText))
	hash := sm3.New()
	hash.Write(textBytes)
	buf := hash.Sum(nil)

	//将加密后的内容以Base64编码成一个字符串,然后返回
	res := base64.StdEncoding.EncodeToString(buf)
	*outParam = C.CString(res)
	*outline = C.int(len(C.GoString(*outParam)))
	return true
}

这里需要注意:
1、import “C” 这一句不能少。
2、被导出的函数,前面得加**//export** 然后加函数名
3、func main() {},虽然没用,但不能少
4、输入的字符串参数类型: *C.char,其实就是C字符串,然后处理的时候,还得转成go字符串
5、输出的字符串参数类型: **C.char
6、输出的整数参数类型:*C.int
7、通常情况下,会缺少两个包。需要在项目终端中运行这两条指令来添加:“go get github.com/tjfoc/gmsm/sm3”、“go get github.com/tjfoc/gmsm/sm4”。要求:科学上网。

编译后,可以得到一个dll文件。

不足之处:本人一边百度,一边写go代码,总算跑起来了。也就是说能用,但绝不是优质代码。网上的示例仅搜到传字符串、传整数,未搜到传数组。因此,最后对最终数据用base64进行了一次封装,当作字符串返回,效率肯定不高,但,能用。

在C#中对dll进行二次封装

如果要在C#中使用这个go语言写的dll,那么需要使用P/Invoke互操作技术,与在C#中直接调用Windows Api一样。但对于C#这种高级的面向对象的语言来说,显得太Low,于是将其封装成两个类,分别对应于SM3、SM4。剪尾巴的工作、16字节对齐的工作,都在这个封装里完成。

在使用这两个类的时候,可直接输入字符串,返回字符串。不需要再关心尾巴、不需要在关心16字节对齐。

C#代码如下:

public static class SM3
    {
        public static string Hash(string plainText)
        {
            var cipherBytes = Encoding.UTF8.GetBytes(plainText);

            IntPtr ptr = IntPtr.Zero;
            int outlen = -1;

            Sm3Hash(cipherBytes, ref ptr, ref outlen);
            if (outlen > 0)
            {
                var result = new byte[outlen];
                Marshal.Copy(ptr, result, 0, outlen);
                var byteString = Encoding.UTF8.GetString(result);
                var result2 = Convert.FromBase64String(byteString);
                return result2.ConvertToHexString();
            }
            return null;
        }

        public static string ConvertToHexString(this byte[] byteArray)
        {
            var sb = new StringBuilder(byteArray.Length);
            for (int i = 0; i < byteArray.Length; i++)
            {
                sb.Append(byteArray[i].ToString("X2"));
            }
            return sb.ToString();
        }

        [DllImport("SM3SM4GO.dll", CharSet = CharSet.Ansi)]
        private static extern bool Sm3Hash(byte[] plainText, ref IntPtr outstr, ref int outlen);
    }

CM4的代码

public static class SM4
    {
        public static string Decrypt(string cipherText, string cipherKey, string cipherIV = null)
        {
            return SM4_De_En_ctypt(false, cipherText, cipherKey, cipherIV);
        }

        public static string Encrypt(string plainText, string cipherKey, string cipherIV = null)
        {
            return SM4_De_En_ctypt(true, plainText, cipherKey, cipherIV);
        }
        private static string SM4_De_En_ctypt(bool enctypt, string cipherText, string cipherKey, string cipherIV = null)
        {
            var cipherBytes = Encoding.UTF8.GetBytes(cipherText);
            var keyBytes = Encoding.UTF8.GetBytes(cipherKey);
            var ivBytes = keyBytes;
            if (cipherIV != null)
            {
                ivBytes = Encoding.UTF8.GetBytes(cipherIV);
            }


            IntPtr ptr = IntPtr.Zero;
            int outlen = -1;
            if (enctypt == true)
            {
                //加密
                Sm4Encrypt(cipherBytes, keyBytes, ivBytes, ref ptr, ref outlen);
                if (outlen > 0)
                {
                    var result = new byte[outlen];
                    Marshal.Copy(ptr, result, 0, outlen);
                    var byteString = Encoding.UTF8.GetString(result);
                    var result2 = Convert.FromBase64String(byteString);
                    return result2.ConvertToHexString();
                }
            }
            else
            {
                //解密
                Sm4Decrypt(cipherBytes, keyBytes, ivBytes, ref ptr, ref outlen);
                if (outlen > 0)
                {
                    var result = new byte[outlen];
                    Marshal.Copy(ptr, result, 0, outlen);
                    var byteString = Encoding.UTF8.GetString(result);
                    var result2 = Convert.FromBase64String(byteString);

                    //查找小尾巴的位置(0x80)
                    int tailIndex = result2.Length - 1;
                    for (; tailIndex >= 0; tailIndex--)
                    {
                        if (result2[tailIndex] == 0x80) break;
                    }

                    //去除小尾巴后面的数据
                    var bytes = new byte[tailIndex];
                    Array.Copy(result2, bytes, tailIndex);
                    return Encoding.UTF8.GetString(bytes);
                }
            }

            return null;
        }

        [DllImport("SM3SM4GO.dll", CharSet = CharSet.Ansi)]
        private static extern bool Sm4Decrypt(byte[] cipherBytes, byte[] keyBytes, byte[] IVBytes, ref IntPtr outstr, ref int outlen);

        [DllImport("SM3SM4GO.dll", CharSet = CharSet.Ansi)]
        private static extern bool Sm4Encrypt(byte[] plainBytes, byte[] keyBytes, byte[] IVBytes, ref IntPtr outstr, ref int outlen);
    }

C#调用

封装完毕后,可以将其编译成一个DLL文件,比如SM3SM4CS.dll(C#二次封装dll),再配合之前SM3SM4GO.dll(go语言编写的dll文件),就可以向下面代码一样简单地完成字符串的加密和解密,而无需关心加密解密的内部实现。

static void Main(string[] args)
        {
            var cipherText = "C06FFCB5FDD40F45B444CDB4E1109DD9F404F85E57ECD0A0E4CF53FE400153E4";
            var key = "NEWCAPECNEWCAPEC";
            //解密 OK
            var data = SM4.Decrypt(cipherText, key);
            Console.WriteLine("解密:"+ data);

            //加密
            var plainText = "This is a plain text to be encrypted!";
            Console.WriteLine("明文:" + plainText);

            var data2 = SM4.Encrypt(plainText, key);
            Console.WriteLine("密文:"+ data2);
            if (data2 != null)
            {
                var data3 = SM4.Decrypt(data2, key);
                Console.WriteLine("解密:" + data3);
            }
            
            var sm3String = SM3.Hash(data2).ToLower();
            Console.WriteLine("签名:"+ sm3String);
        }

结束语

加密解密,终于搞定了,接下来。。。从服务器获得的数据,没有有效内容!
还得折腾。。。

C:\Users\libit\GolandProjects\awesomeProject>C:\Users\libit\Desktop\jiami\Debug\CdutQrCode.exe
post message:   {"cardid":"2022020514"}
resultData:     {"accountinfo":{"cardUserinfo":{}},"cardinfos":[],"qrcodeinfo":{}}
message:        CORE10003
success:        False
client time:    2023/3/13 15:14:06
server time:    2023/3/13 15:14:15 +00:00

你可能感兴趣的:(C#技术,Windows,Go,go,C#,DLL,封装)