Unity接入中宣部防沉迷实名认证之AES-128/GCM + BASE64加密(一)

背景

在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来自测。

Snipaste_2021-04-20_14-53-52.png

最后将中宣部提供的技术操作规范手册中的示例解密出来,而且自己加密并解密也是成功了的。
在此记录一下!

实现

实现一: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官网上,AndroidiOS没有示例代码,有API文档https://www.chilkatsoft.com/reference.asp。

实现二:在Android上实现。

按照官方说明,将libs中的so文件及代码放到相应位置,如下图:

image.png

源码如下:
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));

    }

输出结果:

image.png

实现三:在object-c中实现

根据官网下载的文件,将include文件夹和.a库文件拖进去。


image.png

然后写加解密代码。

源码如下:

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;
}

输出结果:


image.png

最后,Unity中使用

在Unity中通过C#调用Android和iOS中的函数来实现。Android部分代码和上述一下,object-c部分代码也和上述一样。为了方便,我把Android的打成aar库,将object-c的打成framework静态库使用。

后续如何处理,请查看下一篇内容《Unity接入中宣部防沉迷实名认证之最后一步》。

……

2021年4月25日:算了,Chilkat太贵了,后续打算自己实现一个,目前已找到相关资料,还在研究中。

你可能感兴趣的:(Unity接入中宣部防沉迷实名认证之AES-128/GCM + BASE64加密(一))