杂项

当前位置:首页>技术博客>杂项
全部 15 TFrame框架 2 游戏渲染 0 编辑器扩展 0 性能优化 3 SDK 4 数据结构和算法 1 杂项 5

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

时间:2021-06-11   访问量:1695

背景


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

        /// <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太贵了,后续打算自己实现一个,目前已找到相关资料,还在研究中。


上一篇:没有了!

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

发表评论:

评论记录:

未查询到任何数据!