背景
在2021年上半年,中宣部要求接入防沉迷实名认证系统,主要接入四个HTTP请求:上线、下线、实名认证及实名认证查询。
难点
使用UnityWebRequest类进行POST和GET请求,主要难点在于其中要求 AES-128/GCM + BASE64
算法加密。
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来自测。
最后将中宣部提供的技术操作规范手册中的示例解密出来,而且自己加密并解密也是成功了的。
在此记录一下!
实现
实现一:C#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;
///
/// 解密
///
/// 密文
/// 秘钥
/// 明文
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;
}
///
/// 加密
///
/// 明文
/// 秘钥
/// 密文
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.cnblogs.com/qiaoxs/p/14543043.html
但是,上面的是在Windows上,游戏是安卓和iOS,安卓使用的是SDK暂时可以不实现,但iOS需要实现啊!
在Chilkat
官网上,Android
和iOS
没有示例代码,有API文档https://www.chilkatsoft.com/reference.asp。
实现二:在Android上实现。
按照官方说明,将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
#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
#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
#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太贵了,后续打算自己实现一个,目前已找到相关资料,还在研究中。