在2021年上半年,中宣部要求接入防沉迷实名认证系统,主要接入四个HTTP请求:上线、下线、实名认证及实名认证查询。
使用UnityWebRequest类进行POST和GET请求,主要难点在于其中要求 AES-128/GCM + BASE64算法加密。
如果你也是用Unity,可以直接略过这篇文章,去看这一篇文章Unity接入中宣部防沉迷实名认证之AES-128/GCM + BASE64加密(二)
根据查找资料,各种百度、谷歌,针对C#语言,有.netcore和.netstandard的实现,但是要在Unity中使用,.netcore可以排除。而.netstandard要求在2.1版本才存在AesGcm加密,我使用的Unity2019.4版本中,支持的是2.0版本。
最后找了一个插件:Chilkat,有很多语言的版本。注意,这个要购买,30天试用!
本来对AES的GCM加密都不懂,看了上面链接的示例代码,发现各种搞不懂。
最后自己在VS2019中,新建了一个控制台工程,在NuGet包管理中,安装了**chilkat-win32**来自测。
最后将中宣部提供的技术操作规范手册中的示例解密出来,而且自己加密并解密也是成功了的。
在此记录一下!
- 分析
对于密文分为三段,`nonce(12个字节)` + `真正密文` + `tag(16个字节)`。
其中,`nonce`没有啥用,`tag`在解密时需要传入进去,chlkat会验证。
先说一下注意点,经过测试发现:自己加密后再解密,不传aad数据的时候能正常解密,不然的话会解密失败。
- #####源码
using System; using System.Text; namespace AesGcmTest { public class AesGcm { private static readonly int NONCE_LEN = 12; private static readonly int TAG_LEN = 16; /// <summary> /// 解密 /// </summary> /// <param name="cipherText">密文</param> /// <param name="secretKey">秘钥</param> /// <returns>明文</returns> public static string Decrypt(string cipherText, string secretKey) { var plainText = string.Empty; byte[] data = Convert.FromBase64String(cipherText); //nonce是固定12位,被加在密文的前面 byte[] nonce = new byte[NONCE_LEN]; Array.ConstrainedCopy(data, 0, nonce, 0, nonce.Length); //tag是16位,被加在密文的后面 byte[] tag = new byte[TAG_LEN]; Array.ConstrainedCopy(data, data.Length - tag.Length, tag, 0, tag.Length); var tagStr = ByteToHexStr(tag); byte[] cipherTextData = new byte[data.Length - tag.Length - nonce.Length]; Array.ConstrainedCopy(data, nonce.Length, cipherTextData, 0, cipherTextData.Length); byte[] key = StrToHexByte(secretKey); Chilkat.Crypt2 crypt = new Chilkat.Crypt2(); crypt.CryptAlgorithm = "aes"; crypt.CipherMode = "gcm"; crypt.KeyLength = 128; //加密时的输出编码或解密时的输入编码 crypt.EncodingMode = "base64"; crypt.IV = nonce; crypt.SecretKey = key; //解密 crypt.SetEncodedAuthTag(tagStr, "hex"); var resultData = crypt.DecryptBytes(cipherTextData); if (crypt.LastMethodSuccess == true) { plainText = Encoding.UTF8.GetString(resultData); } else { plainText = crypt.LastErrorText; } return plainText; } /// <summary> /// 加密 /// </summary> /// <param name="plainText">明文</param> /// <param name="secretKey">秘钥</param> /// <returns>密文</returns> public static string Encrypt(string plainText, string secretKey) { var cipherText = string.Empty; //nonce是固定12位,被加在密文的前面 Random rand = new Random(); byte[] nonce = new byte[NONCE_LEN]; rand.NextBytes(nonce); string aadTempStr = ""; byte[] aad = Encoding.UTF8.GetBytes(aadTempStr); var aadStr = ByteToHexStr(aad); byte[] key = StrToHexByte(secretKey); var plainTextData = Encoding.UTF8.GetBytes(plainText); Chilkat.Crypt2 crypt = new Chilkat.Crypt2(); crypt.CryptAlgorithm = "aes"; crypt.CipherMode = "gcm"; crypt.KeyLength = 128; //加密时的输出编码或解密时的输入编码 crypt.EncodingMode = "base64"; crypt.IV = nonce; crypt.SecretKey = key; crypt.SetEncodedAad(aadTempStr, ""); //加密 var resultData = crypt.EncryptBytes(plainTextData); if (crypt.LastMethodSuccess == true) { var tagStr = crypt.GetEncodedAuthTag("hex"); //tag是16位,被加在密文的后面 byte[] tag = StrToHexByte(tagStr); if (tag.Length != TAG_LEN) { Console.WriteLine("TAG位数不对!!!!加密后TAG长度:" + tag.Length); } byte[] data = new byte[NONCE_LEN + TAG_LEN + resultData.Length]; Array.ConstrainedCopy(nonce, 0, data, 0, nonce.Length); Array.ConstrainedCopy(resultData, 0, data, nonce.Length, resultData.Length); Array.ConstrainedCopy(tag, 0, data, nonce.Length + resultData.Length, tag.Length); cipherText = Convert.ToBase64String(data); } else { cipherText = crypt.LastErrorText; } return cipherText; } //16进制string转byte[] protected static byte[] StrToHexByte(string hexString) { hexString = hexString.Replace(" ", ""); if ((hexString.Length % 2) != 0) hexString += " "; byte[] returnBytes = new byte[hexString.Length / 2]; for (int i = 0; i < returnBytes.Length; i++) returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16); return returnBytes; } protected static string ByteToHexStr(byte[] bytes) { string returnStr = ""; if (bytes != null) { for (int i = 0; i < bytes.Length; i++) { returnStr += bytes[i].ToString("X2"); } } return returnStr; } } }
参考文章:
- [https://www.example-code.com/csharp/crypt2_aes_gcm.asp](https://www.example-code.com/csharp/crypt2_aes_gcm.asp)
- [https://www.cnblogs.com/qiaoxs/p/14543043.html](https://www.cnblogs.com/qiaoxs/p/14543043.html)
但是,上面的是在Windows上,游戏是安卓和iOS,安卓使用的是SDK暂时可以不实现,但iOS需要实现啊!
在`Chilkat`官网上,`Android`和`iOS`没有示例代码,有API文档https://www.chilkatsoft.com/reference.asp。
####实现二:在Android上实现。
按照[官方](https://www.chilkatsoft.com/chilkatAndroid.asp)说明,将libs中的so文件及代码放到相应位置,如下图:
#####源码如下:
package com.dream.aesgcmcrypt; import android.util.Base64; import android.util.Log; import com.chilkatsoft.CkByteData; import com.chilkatsoft.CkCrypt2; import com.chilkatsoft.CkGlobal; import java.io.UnsupportedEncodingException; public final class AesGcm { private static final String ALGORITHM = "aes"; private static final String CIPHERMODE = "GCM"; private static final String CHARSET = "utf-8"; private static final String ENCODINGMODE = "Base64"; private static final int NONCE_LEN = 12; private static final int TAG_LEN = 16; static { Log.i("AesGcm", "加载chilkat库"); System.loadLibrary("chilkat"); } /** * Chilkat插件解锁 * @param unlockKey */ public static void Init(String unlockKey){ CkGlobal ckGlobal = new CkGlobal(); ckGlobal.UnlockBundle(unlockKey); if (!ckGlobal.get_LastMethodSuccess()){ Log.e("AesGcm","Chilkat解密使用30天试用"); } } /** * AES-GCM-128加密 * @param plainText 明文 * @param secretKey 秘钥 * @return */ public static String Encrypt(String plainText, String secretKey){ String cipherText = ""; CkByteData nonceByteData = new CkByteData(); nonceByteData.appendRandom(NONCE_LEN); CkByteData secretByteData = new CkByteData(); secretByteData.appendEncoded(secretKey, "hex"); CkByteData plainTextByteData = new CkByteData(); plainTextByteData.appendEncoded(plainText,CHARSET); CkCrypt2 ckCrypt = new CkCrypt2(); ckCrypt.put_CryptAlgorithm(ALGORITHM); ckCrypt.put_CipherMode(CIPHERMODE); ckCrypt.put_Charset(CHARSET); ckCrypt.put_SecretKey(secretByteData); ckCrypt.put_IV(nonceByteData); ckCrypt.put_KeyLength(128); ckCrypt.put_EncodingMode(ENCODINGMODE); ckCrypt.SetEncodedAad("",""); CkByteData encryByteData = new CkByteData(); boolean success = ckCrypt.EncryptBytes(plainTextByteData,encryByteData); if (success){ String tagStr = ckCrypt.getEncodedAuthTag("hex"); Log.i("AesGcm", "加密时的tag:" + tagStr); byte[] tagBytes = HexToByte(tagStr); byte[] nonceBytes = nonceByteData.toByteArray(); byte[] encryBytes = encryByteData.toByteArray(); byte[] data = new byte[nonceBytes.length + encryBytes.length + tagBytes.length]; System.arraycopy(nonceBytes,0, data,0,nonceBytes.length); System.arraycopy(encryBytes,0, data,nonceBytes.length,encryBytes.length); System.arraycopy(tagBytes,0, data,nonceBytes.length + encryBytes.length,tagBytes.length); cipherText = Base64.encodeToString(data,0); } else { Log.e("AesGcm", ckCrypt.lastErrorText()); } return cipherText; } /** * AES-GCM-128解密 * @param cipherText 密文 * @param secretKey 秘钥 * @return */ public static String Decrypt(String cipherText, String secretKey){ String plainText = ""; byte[] data = Base64.decode(cipherText,0); byte[] nonceBytes = new byte[NONCE_LEN]; System.arraycopy(data,0,nonceBytes,0,nonceBytes.length); CkByteData nonceByteData = new CkByteData(); nonceByteData.appendByteArray(nonceBytes); CkByteData secretByteData = new CkByteData(); secretByteData.appendEncoded(secretKey, "hex"); byte[] tagBytes = new byte[TAG_LEN]; System.arraycopy(data,data.length-tagBytes.length,tagBytes,0,tagBytes.length); String tagHexStr = BytesToHex(tagBytes); Log.i("AesGcm", "解密时的tag:" + tagHexStr); byte[] cipherBytes = new byte[data.length-tagBytes.length-nonceBytes.length]; System.arraycopy(data,nonceBytes.length,cipherBytes,0,cipherBytes.length); CkCrypt2 ckCrypt = new CkCrypt2(); ckCrypt.put_CryptAlgorithm(ALGORITHM); ckCrypt.put_CipherMode(CIPHERMODE); ckCrypt.put_Charset(CHARSET); ckCrypt.put_SecretKey(secretByteData); ckCrypt.put_IV(nonceByteData); ckCrypt.put_KeyLength(128); ckCrypt.put_EncodingMode(ENCODINGMODE); ckCrypt.SetEncodedAuthTag(tagHexStr,"hex"); CkByteData cipherByteData = new CkByteData(); cipherByteData.appendByteArray(cipherBytes); CkByteData plainTextByteData = new CkByteData(); boolean success = ckCrypt.DecryptBytes(cipherByteData,plainTextByteData); if (success){ byte[] plainTextBytes = plainTextByteData.toByteArray(); try { plainText = new String(plainTextBytes, CHARSET); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } else { Log.e("AesGcm", ckCrypt.lastErrorText()); } return plainText; } static String BytesToHex(byte[] bytes) { StringBuffer sb = new StringBuffer(); for(int i = 0; i < bytes.length; i++) { String hex = Integer.toHexString(bytes[i] & 0xFF); if(hex.length() < 2){ sb.append(0); } sb.append(hex); } return sb.toString(); } static byte[] HexToByte(String hexString){ hexString = hexString.toLowerCase(); final byte[] byteArray = new byte[hexString.length() / 2]; int k = 0; for( int i = 0; i < byteArray.length; i++ ){ //因为是16进制,最多只会占用4位,转换成字节需要两个16进制的字符,高位在先 byte high = (byte) (Character.digit(hexString.charAt(k), 16) & 0xff); byte low = (byte) (Character.digit(hexString.charAt(k + 1), 16) & 0xff); byteArray[i] = (byte) ( high << 4 | low); k += 2; } return byteArray; } }
使用方法:
调用函数`AesGcm.Init(unlockKey)`进行解锁。
调用函数`AesGcm.Decrypt(cipherText, secretKey)`进行解密。
调用函数`AesGcm.Encrypt(plainText, secretKey)`进行加密。
示例:
protected void Test(){ AesGcm.Init("111111111111111"); String plainText = "{"ai":"test-accountId","name":"用户姓名","idNum":"371321199012310912"}"; String cipherText = "CqT/33f3jyoiYqT8MtxEFk3x2rlfhmgzhxpHqWosSj4d3hq2EbrtVyx2aLj565ZQNTcPrcDipnvpq/D/vQDaLKW70O83Q42zvR0//OfnYLcIjTPMnqa+SOhsjQrSdu66ySSORCAo"; String secretKey = "2836e95fcd10e04b0069bb1ee659955b"; Log.i("AesGcm","解密后结果:" + AesGcm.Decrypt(cipherText, secretKey)); String cipherText2 = AesGcm.Encrypt(plainText, secretKey); Log.i("AesGcm","加密后结果:" + cipherText2); Log.i("AesGcm","加密后再解密结果:" + AesGcm.Decrypt(cipherText2, secretKey)); }
输出结果:
####实现三:在object-c中实现
根据官网下载的文件,将include文件夹和.a库文件拖进去。
然后写加解密代码。
#####源码如下:
`AesGcm.h`源码:
```
//
// AesGcm.h
// AesGcmCrypt
//
// Created by yuantao on 2021/4/22.
// Copyright © 2021 dream. All rights reserved.
//
#ifndef AesGcm_h
#define AesGcm_h
#import <Foundation/Foundation.h>
#include "CkoGlobal.h"
#include "CkoCrypt2.h"
#include "CkoBinData.h"
@interface AesGcm : NSObject{
//类变量声明
}
//类属性声明
//类方法声明
+(AesGcm*)get;
-(void)initChilkat:(NSString*)unlockKey;
-(NSData*)hexToBytes:(NSString*)hexString;
-(NSString*)bytesToHex:(NSData*)data;
-(NSData*)base64ToBytes:(NSString*)base64String;
-(NSString*)bytesToBase64:(NSData*)data;
-(NSString*)encrypt:(NSString*)palinText secretKey:(NSString*)secretKey;
-(NSString*)decrypt:(NSString*)cipherText secretKey:(NSString*)secretKey;
@end
#endif /* AesGcm_h */
```
`AesGcm.m`源码:
```
//
// AesGcm.m
// AesGcm
//
// Created by yuantao on 2021/4/22.
// Copyright © 2021 dream. All rights reserved.
//
#import <Foundation/Foundation.h>
#include "AesGcm.h"
@implementation AesGcm
static AesGcm* instance = nil;
NSString* ALGORITHM = @"aes";
NSString* CIPHERMODE = @"GCM";
NSString* CHARSET = @"utf-8";
NSString* ENCODINGMODE = @"Base64";
int NONCE_LEN = 12;
int TAG_LEN = 16;
+ (AesGcm *)get{
if (instance == nil) {
instance = [[AesGcm alloc] init];
}
return instance;
}
- (void)initChilkat:(NSString *)unlockKey{
CkoGlobal *glob = [[CkoGlobal alloc] init];
BOOL success = [glob UnlockBundle:unlockKey];
if (success!=YES) {
NSLog(@"%@",glob.LastErrorText);
return;
}
int status = [glob.UnlockStatus intValue];
if (status == 2) {
NSLog(@"%@",@"Chilkat解密插件使用正式模式.");
}
else {
NSLog(@"%@",@"Chilkat解密插件使用30天试用模式.");
}
}
- (NSString *)encrypt:(NSString *)plainText secretKey:(NSString *)secretKey{
NSString* cipherText = @"";
CkoBinData* plainTextBinData = [[CkoBinData alloc] init];
[plainTextBinData AppendString:plainText charset:CHARSET];
NSData* plainTextData = [plainTextBinData GetBinary];
NSData* secretKeyData = [self hexToBytes:secretKey];
CkoBinData* nonceBinData = [[CkoBinData alloc] init];
for (int i = 0; i < NONCE_LEN; i++) {
[nonceBinData AppendByte:[NSNumber numberWithInt:arc4random()%NONCE_LEN]];
}
NSData* nonce = [nonceBinData GetBinary];
CkoCrypt2 *crypt = [[CkoCrypt2 alloc] init];
crypt.CryptAlgorithm = ALGORITHM;
crypt.CipherMode = CIPHERMODE;
crypt.Charset = CHARSET;
crypt.SecretKey = secretKeyData;
crypt.IV = nonce;
crypt.KeyLength = [NSNumber numberWithInt:128];
crypt.EncodingMode = ENCODINGMODE;
[crypt SetEncodedAad:@"" encoding:@""];
NSData* encryData = [crypt EncryptBytes:plainTextData];
if (encryData!=nil) {
NSString* tag = [crypt GetEncodedAuthTag:@"hex"];
NSLog(@"加密时的tag:%@", tag);
NSData* tagData = [self hexToBytes:tag];
NSMutableData* cipherData = [[NSMutableData alloc] init];
[cipherData appendData:nonce];
[cipherData appendData:encryData];
[cipherData appendData:tagData];
cipherText = [self bytesToBase64:cipherData];
}
else{
NSLog(@"%@", [crypt LastErrorText]);
}
return cipherText;
}
- (NSString *)decrypt:(NSString *)cipherText secretKey:(NSString *)secretKey{
NSString* plainText = @"";
NSData* data = [self base64ToBytes:cipherText];
NSData* nonce = [data subdataWithRange:NSMakeRange(0, NONCE_LEN)];
NSData* tag = [data subdataWithRange:NSMakeRange(data.length - TAG_LEN, TAG_LEN)];
NSString* tagStr = [self bytesToHex:tag];
NSLog(@"解密时的tag:%@", tagStr);
NSData* cipher = [data subdataWithRange:NSMakeRange(NONCE_LEN, data.length-TAG_LEN-NONCE_LEN)];
NSData* secretKeyData = [self hexToBytes:secretKey];
CkoCrypt2 *crypt = [[CkoCrypt2 alloc] init];
crypt.CryptAlgorithm = ALGORITHM;
crypt.CipherMode = CIPHERMODE;
crypt.Charset = CHARSET;
crypt.SecretKey = secretKeyData;
crypt.IV = nonce;
crypt.KeyLength = [NSNumber numberWithInt:128];
crypt.EncodingMode = ENCODINGMODE;
[crypt SetEncodedAuthTag:tagStr encoding:@"hex"];
NSData* plainTextData = [crypt DecryptBytes:cipher];
if (plainTextData!=nil) {
plainText = [[NSString alloc] initWithData:plainTextData encoding:NSUTF8StringEncoding];
}
else{
NSLog(@"%@", [crypt LastErrorText]);
}
return plainText;
}
- (NSString *)bytesToBase64:(NSData *)data{
NSData* base64Data = [data base64EncodedDataWithOptions:0];
NSString* result = [[NSString alloc] initWithData:base64Data encoding:NSUTF8StringEncoding];
return result;
}
- (NSData *)base64ToBytes:(NSString *)base64String{
CkoBinData* binData = [[CkoBinData alloc] init];
[binData AppendString:base64String charset:@"Base64"];
return [binData GetBinary];
}
- (NSData *)hexToBytes:(NSString *)hexString{
CkoBinData* binData = [[CkoBinData alloc] init];
[binData AppendString:hexString charset:@"hex"];
return [binData GetBinary];
}
- (NSString *)bytesToHex:(NSData*)data{
if (!data || [data length] == 0) {
return @"";
}
NSMutableString *string = [[NSMutableString alloc] initWithCapacity:[data length]];
[data enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop) {
unsigned char *dataBytes = (unsigned char*)bytes;
for (NSInteger i = 0; i < byteRange.length; i++) {
NSString *hexStr = [NSString stringWithFormat:@"%x", (dataBytes[i]) & 0xff];
if ([hexStr length] == 2) {
[string appendString:hexStr];
} else {
[string appendFormat:@"0%@", hexStr];
}
}
}];
return string;
}
@end
```
使用方法:
调用`[[AesGcm get] initChilkat:unlockKey];`进行插件API解锁,不然的话就是30天试用。
调用`[[AesGcm get] decrypt:cipherText secretKey:secretKey];`进行解密。
调用`[[AesGcm get] encrypt:plainText secretKey:secretKey];`进行加密。
示例:
```
#import <Foundation/Foundation.h>
#include "AesGcm.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSString* unlockKey = @"Anything for 30-day trial";
[[AesGcm get] initChilkat:unlockKey];
NSString* secretKey = @"2836e95fcd10e04b0069bb1ee659955b";
NSString* plainText =@"{"ai":"test-accountId","name":"用户姓名","idNum":"371321199012310912"}";
NSString* cipherText = @"CqT/33f3jyoiYqT8MtxEFk3x2rlfhmgzhxpHqWosSj4d3hq2EbrtVyx2aLj565ZQNTcPrcDipnvpq/D/vQDaLKW70O83Q42zvR0//OfnYLcIjTPMnqa+SOhsjQrSdu66ySSORCAo";
NSString* result = [[AesGcm get] decrypt:cipherText secretKey:secretKey];
NSLog(@"示例解密结果:%@", result);
result = [[AesGcm get] encrypt:plainText secretKey:secretKey];
NSLog(@"加密结果:%@", result);
result = [[AesGcm get] decrypt:result secretKey:secretKey];
NSLog(@"解密结果:%@", result);
}
return 0;
}
```
输出结果:
####最后,Unity中使用
在Unity中通过C#调用Android和iOS中的函数来实现。Android部分代码和上述一下,object-c部分代码也和上述一样。为了方便,我把Android的打成aar库,将object-c的打成framework静态库使用。
后续如何处理,请查看下一篇内容《Unity接入中宣部防沉迷实名认证之最后一步》。
……
2021年4月25日:算了,Chilkat太贵了,后续打算自己实现一个,目前已找到相关资料,还在研究中。
上一篇:没有了!