前言
对于开发人员,开发中加解密是经常用到的,常见的密码算法 MD5、SHA、AES、DES,RSA 等等,这些无一例外都是国外的加密算法。基于安全和宏观战略考虑,我国从 2010 年先后推出了 SM1(SCB2)、SM2、SM3、SM4、SM7、SM9、ZUC(祖冲之密码算法)等密码算法,本文主要讨论 SM2 算法原理,iOS 端如何使用 SM2、SM4 加解密、SM2 签名验签及使用 SM3 生成摘要值。
国密全家桶
国密算法中,SM1、SM4、SM7、ZUC 是对称算法;SM2、SM9是非对称算法;SM3是哈希算法。其中 SM1 和 SM7 分组密码算法不公开,SM1 主要用于加密芯片等重要领域,例如 智能 IC 卡,加密机等;SM7 主要用于常规非接触式 IC 卡,例如门禁卡,工作证等。
算法 | 公开 | 类似 | 主要用途 |
---|---|---|---|
SM1 | 否 | AES | 智能IC卡、加密卡,加密机等。 |
SM2 | 是 | RSA | 重要信息的加解密,签名,如密码。 |
SM3 | 是 | SHA | 密码应用中的数字签名和验证,摘要等。 |
SM4 | 是 | AES | 分组算法用于无线局域网产品。 |
SM7 | 否 | AES | 校园一卡通,门禁卡,工作证等。 |
SM9 | 是 | SSL | 基于身份的密码,用于验证身份。 |
ZUC | 是 | AES | 4G 网络中的国际标准密码算法。 |
SM2 算法原理
SM2 算法是国密标准的非对称算法标准,基于ecc(Elliptic Curves Cryptography,椭圆曲线密码编码学)的扩展。提起非对称加密,自然想到了 RSA,对极大整数做因数分解的难度决定了RSA算法的可靠性(RSA 算法理解),这是 RSA 安全的基础。那国密加解密的算法基础是什么?首先我们先理解一下椭圆曲线。
SM2 椭圆曲线
国密 SM2 的算法基础是椭圆曲线,公式:
y^2 = x^3 + ax + b(4a^3 + 27b^2 ≠ 0)
那椭圆曲线长什么样子呢,百闻不如一见,图片能直观感受。
为什么需要满足呢?
4a^3 + 27b^2 ≠ 0
因为当这个公式等于 0 时,它不是椭圆曲线。
SM2 算法理解
结合上面这张图,我们了解一下 SM2 的几何意义。
SM2 公私钥
算法是基于数学的,SM2 定义曲线上的群运算加减乘,通过公私钥的生成理解。
- 首选一条椭圆曲线,即固定 a、b 的值,假设选择的是上图所示曲线。
- 随机选择一个点 P 为基点,曲线做切线,经过 Q 点,切点 R1。
- 基于 x 轴做 R1 的对称点 R,则 SM2 定义加法为 P + Q = R,这就是椭圆曲线加法。
- 求 2 倍点,当 P = Q 时,即 P + P = R = 2P,则 R 是 P 的 2 倍点。
- 求 3 倍点,3P = P + 2P = P + R,经过 P、R 做直线,交于椭圆曲线点 M1, 基于 x 轴对称点 M 则是 3 倍点,依次类推。
- 求 d 倍点,假设我们同样次数为 d,运算倍点为 Q。
- d 为私钥,Q 为公钥。所以私钥是一个大整数,公钥是一个点坐标。
上面的几何推理是为了方便理解,实际取值都是在质数有限域上。密码专家们经过推理和运算,已经为我们选择了质数有限域上的最优椭圆曲线,除非有特殊需要,否则不需要自定义曲线。
p:椭圆曲线在质数 p 的有限域 Fp 上的点集合;
a:椭圆曲线参数 a 的值;
b:椭圆曲线参数 b 的值;
n:取值范围,随机整数 d 的取值范围 [1,n-2];
Gx:基点的 x 坐标值,类似于点 P 的 x 坐标值;
Gy:基点的 y 坐标值,类似于点 P 的 y 坐标值。
SM2 加密
SM2 加密结果长度是固定的,例如密码为 123456 的 6 位数字,加密结果长度 = 64 + 32 + 6 = 102 字节,转为 16 进制字符串结果为 204 个字符。原文长度为 n,则加密结果长度 r = 96 + n。
加密过程:
设椭圆曲线为推荐曲线,公钥 Q,原文比特串 M,klen 为 M 的比特长度;
- 计算随机椭圆曲线点 C1 = [k]G=(x1, y1),k 是随机数,G为基点,计算出的倍点 C1 为 64 字节;
- 校验公钥 Q,计算椭圆曲线点 S=[h]Q,h为余因子,若S 为无穷点,退出;
- 计算椭圆曲线点 [k]PB=(x2, y2),获取 x2,y2;
- 计算 t = KDF(x2||y2,klen),若 t 为全 0 比特串,则返回步骤 1,KDF是 SM2 的密钥派生函数;
- 计算 C2= M⊕t,对明文加密,C2 是真正的密文,长度和原文相同;
- 计算 C3= Hash (x2||M|| y2),生成杂凑值,用来效验数据,长度 32 字节;
- 输出密文 C=C1||C3||C2,C 为密文结果。
注意:OpenSSL 加密结果是经过 ASN1 格式化编码的,加密结果长度会不固定。加过过程中使用了随机数,所以每次加密结果都不一样。
SM2 解密
SM2 解密就是逆流程走一遍,注意 OpenSSL 解密要求传入的密文是 ASN1 编码的。
设椭圆曲线为推荐曲线 私钥 d,密文 C(C=C1||C3||C2),klen 为密文中 C2 的比特长度。
- 从 C 中取出比特串 C1(密文 C 的前 64 字节),将 C1 的数据类型转换为椭圆曲线上的点,验证 C1 是否满足椭圆曲线方程,若不满足则报错并退出;
- 计算椭圆曲线点 S= [h]C1,若 S 是无穷远点,则报错并退出;
- 计算[d]C1=(x2, y2),将坐标 x2、y2 的数据类型转换为比特串;
- 计算 t = KDF(x2||y2,klen),若 t 为全 0 比特串,则报错并退出;
- 从 C 中取出比特串 C2,计算 M’=C2⊕t;
- 计算 u = Hash (x2||M’|| y2),从 C 中取出比特串 C3(密文 C 的后 32 字节),若 u≠C3,则报错并退出;
- 输出明文 M’,M’ 就是解密后的明文。
集成 OpenSSL
OpenSSL 1.1.1 以上版本增加了对 SM2/SM3/SM4 密码算法的支持,我们可以直接使用 OpenSSL 实现国密加解密。需要注意的是,OpenSSL 没有官方版本的 cocoapods 版本,我们需要自行将 OpenSSL 编译为 framework。然而,当检查打包完成的静态库时,发现并未暴露国密的头文件,解决办法很简单,打开下载的 OpenSSL 源码,将 crypto/include/internal 路径下的 sm2.h、sm3.h,sm4.h 都拖到 openssl.framework/Headers 文件夹下即可。
如果想通过 cocoapods 集成 OpenSSL,或者不会编译,我已经将编译完成的 OpenSSL.framework 上传至 cocoapods,编辑 Podfile 文件,添加 pod 'GMObjC'
,保存执行 pod install
即可。
若想自行编译,在 GitHub 有开源的编译脚本 https://github.com/muzipiao/GMOpenSSL,下载根据说明编译即可。
国密的 Objective-C 封装
OpenSSL 实现了 SM2/SM3/SM4 密码算法,但没有注释说明,且纯 C 的 API 用起来不方便。所以,对 SM2/SM3/SM4 进行了 Objective-C 封装,方便在 iOS 端使用。
具体封装过程不再详解,开源项目,可自行查看源码。实现过程有点坎坷,尤其 SM2 加解密,后台是对 C1||C3||C2 拼接的原始密文进行操作,而 OpenSSL 加解密都是 ASN1 编码格式,还好 OpenSSL 是开源项目,查看源码找到了原因。
查看具体实现过程,请至开源项目地址https://github.com/muzipiao/GMObjC。
sm2 加解密
sm2 加解密,加密传入待加密字符串和公钥,解密传入密文和私钥即可,代码:
// 公钥
NSString *gPubkey = @"0408E3FFF9505BCFAF9307E665E9229F4E1B3936437A870407EA3D97886BAFBC9C624537215DE9507BC0E2DD276CF74695C99DF42424F28E9004CDE4678F63D698";
// 私钥
NSString *gPrikey = @"90F3A42B9FE24AB196305FD92EC82E647616C3A3694441FB3422E7838E24DEAE"
// 待加密的字符串
NSString *pwd = @"123456";
// 加密
NSString *ctext = [GMSm2Utils encrypt:pwd PublicKey:gPubkey];
// 解密
NSString *plainText = [GMSm2Utils decrypt:encodeCtext PrivateKey:gPrikey];
注意:
- OpenSSL 所用公钥是 04 开头的,后台返回公钥可能是不带 04 的,需要手动拼接。
- 后台返回的解密结果可能是没有标准编码的原始密文,而 OpenSSL 的加解密都是需要 ASN1 编码格式,所以与后台交互过程中,可能需要 ASN1 编码解码。
sm2 签名验签
sm2 私钥签名,公钥验签,可防篡改或验证身份。签名时传入明文、私钥和用户 ID;验签时传入明文、签名、公钥和用户 ID,代码:
// 公钥
NSString *gPubkey = @"0408E3FFF9505BCFAF9307E665E9229F4E1B3936437A870407EA3D97886BAFBC9C624537215DE9507BC0E2DD276CF74695C99DF42424F28E9004CDE4678F63D698";
// 私钥
NSString *gPrikey = @"90F3A42B9FE24AB196305FD92EC82E647616C3A3694441FB3422E7838E24DEAE"
// 待签名的原文
NSString *pwd = @"123456";
// 这里传入自定义 ID,和服务器保持两端一致即可。
NSString *userID = @"lifei_zdjl@126.com";
// 签名结果(r+s)拼接的 16 进制字符
NSString *signStr = [GMSm2Utils sign:pwd PrivateKey:gPrikey UserID:userID];
// 验签,isOK 为 YES 验签通过,NO 为未通过
BOOL isOK = [GMSm2Utils verify:pwd Sign:signStr PublicKey:self.gPubkey UserID:userID];
// 对签名结果 Der 编码
NSString *derSign = [GMSm2Utils encodeWithDer:signStr];
// 对 Der 编码解码
NSString *originStr = [GMSm2Utils decodeWithDer:derSign];
注意:
- 用户 ID 可传空值,当传空值时使用 OpenSSL 默认用户 ID,OpenSSL 中默认用户定义为
#define SM2_DEFAULT_USERID "1234567812345678"
,客户端和服务端用户 ID 要保持一致。 - 客户端和后台交互的过程中,假设后台签名,客户端验签,后台返回的签名是 DER 编码格式,就需要先对签名进行 DER 解码,然后再进行验签。同理,若客户端签名,后台验签,根据后台是需要 (r, s) 拼接格式签名,还是 DER 格式,进行编码解码。
sm4 加解密
sm4 加解密都很简单,加密传入待加密字符串和密钥,解密传入密文和密钥即可,代码:
// 待加密字符串
NSString *pwd = @"123456";
// 生产 sm4 密钥,注意为 16 字节字母数字符号混合的字符串
NSString *sm4Key = [GMSm4Utils createSm4Key]; // 生成16位密钥
// sm4 加密
NSString *sm4Ctext = [GMSm4Utils encrypt:pwd Key:sm4Key];
// sm4 解密
NSString *sm4Ptext = [GMSm4Utils decrypt:sm4Ctext Key:sm4Key];
sm3 摘要
类似于 hash、md5,sm3 摘要算法可对文本文件进行摘要计算,摘要长度为 64 个字符的字符串格式。
// 待提取摘要的字符串
NSString *pwd = @"123456";
// 字符串的摘要
NSString *pwdDigest = [GMSm3Utils hashWithString:plainText];
// 对文件进行摘要计算,传入 NSData 即可
NSString *txtPath = [[NSBundle mainBundle] pathForResource:@"sm4TestFile.txt" ofType:nil];
NSData *fileData = [NSData dataWithContentsOfFile:txtPath];
// 文件的摘要值
NSString *fileDigest = [GMSm3Utils hashWithData:self.fileData];
ASN1 编码解码
OpenSSL 对 sm2 加密结果进行了 ASN1 编码,解密时也是要求密文编码格式为 ASN1 格式,其他平台加解密可能需要 C1C3C2 拼接的原始密文,所以需要编码解码,代码:
// ASN1 编码的密文
NSString *ctext = @"30:6F:02:21:00:D4:F1:B3:2E:29:50:1E:94:44:46:7F:9E:2E:51:36:1E:91:F5:EC:0B:96:F3:34:94:E5:50:82:9F:00:CC:B5:B7:02:20:04:42:83:DF:76:21:B2:9C:EB:7F:64:8B:B4:7A:3C:BF:FE:97:47:E4:D2:BD:47:44:C9:DA:1D:68:12:23:43:D6:04:20:45:F6:AB:54:22:71:63:93:95:3B:58:E3:8D:90:32:B7:A1:D8:76:2B:B8:16:F2:6A:83:51:77:44:2D:28:2C:D2:04:06:62:9F:38:6A:77:76";
// 对 ASN1 编码的密文解码
NSString *decodeStr = [GMSm2Utils decodeWithASN1:ctext];
// 原始密文(C1C3C2 直接拼接)
NSString *dCtext = @"D4F1B32E29501E9444467F9E2E51361E91F5EC0B96F33494E550829F00CCB5B7044283DF7621B29CEB7F648BB47A3CBFFE9747E4D2BD4744C9DA1D68122343D645F6AB5422716393953B58E38D9032B7A1D8762BB816F26A835177442D282CD2629F386A7776";
// 对 C1C3C2 直接拼接的原始密文 ASN1 编码
NSString *encodeStr = [GMSm2Utils encodeWithASN1:dCtext];
生成公私钥
基于 sm2 推荐曲线(素数域 256 位椭圆曲线),生成公私钥。
// 生成公私钥对,数组元素 1 为公钥,2 为私钥
NSArray *newKey = [GMSm2Utils createPublicAndPrivateKey];
// 公钥
NSString *pubKey = newKey[0];
// 私钥
NSString *priKey = newKey[1];
参考
其他
如果您觉得有所帮助,请在 GitHub GMObjC 上赏个Star ⭐️,您的鼓励是我前进的动力。