说明

当前框架

事实证明,基于 AES 的 GCM 加密模式是安全可靠的,并且目前常用于小游戏网络传输的加密

信息加密基础及加密模式 | 一只大菜狗 (cai.dog)

对于客户端来说,使用的是Cocos Creator,使用语言为TypeScript

对于服务器来说,使用的是Asp .Net Core的框架,使用语言为C#

关于 AES-GCM

高级加密标准(英语:Advanced Encryption Standard,缩写:AES),又称 Rijndael 加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的 DES,已经被多方分析且广为全世界所使用。经过五年的甄选流程,高级加密标准由美国国家标准与技术研究院(NIST)于 2001 年 11 月 26 日发布于 FIPS PUB 197,并在 2002 年 5 月 26 日成为有效的标准。

现在,高级加密标准已然成为对称密钥加密中最流行的算法之一。 aes-192 密钥的长度为 24 字节,aes-256 密钥的长度为 32 字节,aes-128 密码的长度为 16 字节。aes-gcm 需要 key,nonce,adata,另外 aes-gcm 不需要填充。

GCM ( Galois/Counter Mode) 指的是该对称加密采用 Counter 模式,并带有 GMAC 消息认证码。

Key 密钥

在标准规范中长度范围必须为 16、24、32 字节(128, 192, 256bits)之一,加密和解密相同,是双方约定的。

nonce 初始向量

初始向量,在 AES-GCM 中写 nonce,读英文本意的意思就是随机的,暂时的意思,别的加密模式一般也写作 IV,它也可以看做秘钥的一部分,加密和解密都需要传入,主要用于防止攻击者掌握密钥后对密文的破解。

nonce 它在加密的时候通过某种算法生成,一般是生成的随机数,并通过某种方式发送给解密方。长度范围为 AesGcm.NonceByteSizes

在标准规范中,nonce 的长度为 12 字节。

Tag

tag 是在加密的过程中生成,解密的时候需要使用,一般认为是密文的一部分。

接收生成的身份验证标记的字节数组,取值范围为 AesGcm.TagByteSizes。

在标准规范中,tag 的长度为 16 字节。

也就是所说的 MAC 验证码,用传统的理解,就像调兵的虎符的一部分,如果在解密时对上了,才说明这个消息的来源是正确的,才能信任使者带来的圣旨

adata 就是 associatedData

在标准规范中,adata 的长度没有要求。

加密时,需要填写明文、密码、nonce、选填 adata,输出密文与 Tag。

解密时,需要填写密码、nonce、密文、Tag,adata 根据实际情况填写,得到明文

所有数据都为 HexString 格式

基本图未如下

可以参考

aes-gcm 在线加密工具 (const.net.cn)

客户端加密解密

对于JavaScript来说,有挺多加密解密的库,并且对Cocos Creator 3.5 之前,应该是直接可以使用 J 语言的所有库,但如果对于使用 Ts 来说,可能一部分直接基于NodeJs的,一部分可能并没有实现GCM的加密模式

几个实现方法参考

(1)有一个比较好的参考为:使用 SJCL 实现 AES GCM 加解密 | 肉饼博客 (roubin.me)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
SJCL = {
str2hex(str) {
if (str === "") return "";
let arr = [];
for (let i = 0; i < str.length; i++) {
arr.push(str.charCodeAt(i).toString(16));
}
return arr.join("");
},

rightPad(str, targetLength, padChar) {
return str + Array(targetLength - str.length + 1).join(padChar);
},

getKey(pd) {
if (pd) {
const pdLen = pd.length;
if (pdLen > 32) pd = pd.slice(0, 32);
else if (pdLen > 24 && pdLen < 32) pd = pd.slice(0, 24);
else if (pdLen > 16 && pdLen < 24) pd = pd.slice(0, 16);
else if (pdLen < 16) pd = this.rightPad(pd, 16, "0");
} else {
pd = this.rightPad("", 32, "0");
}
return sjcl.codec.hex.toBits(this.str2hex(pd));
},

// 12 bytes base64编码的iv
getRandomIv() {
const ivBits = sjcl.random.randomWords(3);
return sjcl.codec.base64.fromBits(ivBits);
},

encrypt(pd, data) {
const key = this.getKey(pd);
const iv = this.getRandomIv();
const encryptedData = sjcl.encrypt(key, JSON.stringify(data), {
mode: "gcm",
ts: 128,
iv,
});
return _.pick(JSON.parse(encryptedData), ["iv", "ct"]);
},

decrypt(pd, data) {
const key = this.getKey(pd);
const encryptedData = Object.assign(data, { mode: "gcm", ts: 128 });
const plainText = sjcl.decrypt(key, JSON.stringify(encryptedData));
return JSON.parse(plainText);
},
};

(2)还有一个参考为使用CryptoJS ,具体百度一大堆

(3)另外还有一个 Js 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const GCM_NONCE_LENGTH = 12 * 8;
const GCM_TAG_LENGTH = 16 * 8;

// Separate IV and ciptertext/tag combination
let ivCiphertextTagB64 =
"2wLsVLuOJFX1pfwwjoLhQrW7f/86AefyZ7FwJEhJVIpU+iG2EITzushCpDRxgqK2cwVYvfNt7KFZ39obMMmIqhrDCIeifzs=";
let ivCiphertextTag = sjcl.codec.base64.toBits(ivCiphertextTagB64);
let iv = sjcl.bitArray.bitSlice(ivCiphertextTag, 0, GCM_NONCE_LENGTH);
let ciphertextTag = sjcl.bitArray.bitSlice(ivCiphertextTag, GCM_NONCE_LENGTH);

// Derive key via SHA256
let key = sjcl.hash.sha256.hash("my password");

// Decrypt
let cipher = new sjcl.cipher.aes(key);
let plaintext = sjcl.mode.gcm.decrypt(
cipher,
ciphertextTag,
iv,
null,
GCM_TAG_LENGTH
);
//let plaintext = sjcl.mode.gcm.decrypt(cipher, ciphertextTag, iv) // works also; here the defaults for the AAD ([]) and the tag size (16 bytes) are applied
console.log(sjcl.codec.utf8String.fromBits(plaintext));

(4)一个比较完整的直接转成 hexstring 再直接算一次的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let key = "[base64 key]";
let cipherText = "[base64 encrypted data]";
const GCM_TAG_LENGTH = 16 * 8;
const GCM_AAD_LENGTH = 16 * 8;
const GCM_NONCE_LENGTH = 12 * 8;

let bkey = sjcl.codec.base64.toBits(key);
let bdata = sjcl.codec.base64.toBits(cipherText);
let cipher = new sjcl.cipher.aes(bkey);

let aad = sjcl.bitArray.bitSlice(bdata, 0, GCM_AAD_LENGTH);
let iv = sjcl.bitArray.bitSlice(aad, GCM_NONCE_LENGTH * -1);
let tag = sjcl.bitArray.bitSlice(bdata, GCM_TAG_LENGTH * -1);
let data = sjcl.bitArray.bitSlice(bdata, GCM_AAD_LENGTH);
let decryptedContent = "";

let decbits = sjcl.mode.gcm.decrypt(cipher, data, iv, aad, GCM_TAG_LENGTH);
decryptedContent = sjcl.codec.utf8String.fromBits(decbits);
console.log("decryptedContent", decryptedContent);

(5)另外一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// http://bitwiseshiftleft.github.io/sjcl/doc/

var crypto = require('crypto'),
sjcl = require('sjcl');

//----------------------------------------------------------------------

var key = new Buffer('RB9xQTAaqvRs8OW4t5/M/RBWEAjq/ur73EeDV06cfGI=', 'base64'),
iv = new Buffer('jwM1m/dPQnA5Ke9I', 'base64'),

msg = new Buffer('The SJCL library works.', 'utf8');

//----------------------------------------------------------------------

var cipher = crypto.createCipheriv('aes-256-gcm', key, iv),
ct = Buffer.concat([ cipher.update(msg), cipher.final(), cipher.getAuthTag() ]);

console.log('[node]', ct.toString('base64'));

//----------------------------------------------------------------------

var b64 = sjcl.codec.base64,
password = b64.toBits(key.toString('base64')),
ivBits = b64.toBits(iv.toString('base64')),
message = b64.toBits(msg.toString('base64')),

aes = new sjcl.cipher.aes(password),
ct = sjcl.mode.gcm.encrypt(aes, message, ivBits);

console.log('[sjcl]', b64.fromBits(ct));

//----------------------------------------------------------------------

var ctBits = ct,
ctBuf = new Buffer(b64.fromBits(ct), 'base64');

var ciphertext = ctBuf.slice(0, ctBuf.length - 16),
authTag = ctBuf.slice(ctBuf.length - 16, ctBuf.length),
decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);

decipher.setAuthTag(authTag);
var pt = Buffer.concat([ decipher.update(ciphertext), decipher.final() ]);

console.log('[node]', pt.toString());

//----------------------------------------------------------------------

var pt = sjcl.codec.utf8String.fromBits(sjcl.mode.gcm.decrypt(aes, ctBits, ivBits));

console.log('[sjcl]', pt);

大概原理都是一样的,最后直接调用到一个接口就可以,这个接口一般都是如上文所说,提供 hexstring 的 cipher, data, iv, aad 即可

服务器加密解密

对于Asp .Net Core来说,在.Net 3.0之后

微软在System.Security.Cryptography提供了一套AesGcm 的加密解密实现AesGcm 类 (System.Security.Cryptography) | Microsoft Learn

直接书写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System.Security.Cryptography;

public (byte[], byte[]) Encrypt(byte[] dataToEncrypt, byte[] key, byte[] nonce, byte[] associatedData)
{
// these will be filled during the encryption
byte[] tag = new byte[16];

//这里的密文缓存和明文长度一样即可
byte[] ciphertext = new byte[dataToEncrypt.Length];

using (AesGcm aesGcm = new AesGcm(key))
{
aesGcm.Encrypt(nonce, dataToEncrypt, ciphertext, tag, associatedData);
}

return (ciphertext, tag);
}

public byte[] Decrypt(byte[] cipherText, byte[] key, byte[] nonce, byte[] tag, byte[] associatedData)
{
//这里的密文缓存和明文长度一样即可
byte[] decryptedData = new byte[cipherText.Length];

using (AesGcm aesGcm = new AesGcm(key))
{
aesGcm.Decrypt(nonce, cipherText, tag, decryptedData, associatedData);
}

return decryptedData;
}

另外,还有一个实现是使用BouncyCastle 实现 RSA 算法,具体网上一大堆

这里有一个很不错的.Net 实现的 Git 地址

stephenhaunts/Building-Secure-Applications-with-Cryptography-in-.NET-Course-Source-Code: The source code for the Pluralsight course, Building Secure Applications with Cryptography in .NET (github.com)

如何实现消息发送和接收

如果要消息接收,那就是得把加密得到的密文和MAC如何发送

目前使用的方法是直接把他们连接,然后再转成 Base64 发送和接收

直接使用sjcl.js的方法为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public static encryption(params) {

//生成一个12位的随机Iv
var iv = sjcl.random.randomWords(3, 0);

var key = sjcl.codec.hex.toBits(this.keyString);
var cipher = new sjcl.cipher.aes(key);
var data = sjcl.codec.utf8String.toBits(params);
var enc = sjcl.mode.gcm.encrypt(cipher, data, iv, {}, 128);

//把得到的Iv,也就是Mac和加密后的内容连接起来
var concatbitArray = sjcl.bitArray.concat(iv, enc);

//再转成Base64发送
var conString = sjcl.codec.base64.fromBits(concatbitArray);
return conString;
}

public static decryptor(content) {

//先从Base64转hexstring
var bitArray = sjcl.codec.base64.toBits(content);
var bitArrayCopy = bitArray.slice(0);

//取前3位,取出Iv
var ivdec = bitArrayCopy.slice(0, 3);

//后面的为密文
var encryptedBitArray = bitArray.slice(3);
var key = sjcl.codec.hex.toBits(this.keyString);
let cipher = new sjcl.cipher.aes(key);
var data = sjcl.mode.gcm.decrypt(cipher, encryptedBitArray, ivdec, {}, 128);
var str = sjcl.codec.utf8String.fromBits(data);
return str;
}

服务器使用.NetCore 的加密解密方法为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

public byte[] DecryptWithKey(byte[] encryptedMessage, byte[] key, int nonSecretPayloadLength = 0)
{
//User Error Checks
CheckKey(key);

if (encryptedMessage == null || encryptedMessage.Length == 0)
{
throw new ArgumentException("Encrypted Message Required!", "encryptedMessage");
}

using (var cipherStream = new MemoryStream(encryptedMessage))
using (var cipherReader = new BinaryReader(cipherStream))
{
//Grab Payload
var nonSecretPayload = cipherReader.ReadBytes(nonSecretPayloadLength);

//Grab Nonce
var nonce = cipherReader.ReadBytes(_nonceSize / 8);

var cipher = new GcmBlockCipher(new AesEngine());
var parameters = new AeadParameters(new KeyParameter(key), _macSize, nonce, nonSecretPayload);
cipher.Init(false, parameters);

//Decrypt Cipher Text
var cipherText =
cipherReader.ReadBytes(encryptedMessage.Length - nonSecretPayloadLength - nonce.Length);
var plainText = new byte[cipher.GetOutputSize(cipherText.Length)];

var len = cipher.ProcessBytes(cipherText, 0, cipherText.Length, plainText, 0);
cipher.DoFinal(plainText, len);

return plainText;
}
}

public byte[] EncryptWithKey(byte[] messageToEncrypt, byte[] key, byte[] nonSecretPayload = null)
{
//User Error Checks
CheckKey(key);

//Non-secret Payload Optional
nonSecretPayload = nonSecretPayload ?? new byte[] { };

//Using random nonce large enough not to repeat
var nonce = new byte[_nonceSize / 8];
_random.NextBytes(nonce, 0, nonce.Length);

var cipher = new GcmBlockCipher(new AesEngine());
var parameters = new AeadParameters(new KeyParameter(key), _macSize, nonce, nonSecretPayload);
cipher.Init(true, parameters);

//Generate Cipher Text With Auth Tag
var cipherText = new byte[cipher.GetOutputSize(messageToEncrypt.Length)];
var len = cipher.ProcessBytes(messageToEncrypt, 0, messageToEncrypt.Length, cipherText, 0);
cipher.DoFinal(cipherText, len);

//Assemble Message
using (var combinedStream = new MemoryStream())
{
using (var binaryWriter = new BinaryWriter(combinedStream))
{
//Prepend Authenticated Payload
binaryWriter.Write(nonSecretPayload);
//Prepend Nonce
binaryWriter.Write(nonce);
//Write Cipher Text
binaryWriter.Write(cipherText);
}

return combinedStream.ToArray();
}
}

private void CheckKey(byte[] key)
{
if (key == null || key.Length != _keySize / 8)
{
throw new ArgumentException(
String.Format("Key needs to be {0} bit! actual:{1}", _keySize, key?.Length * 8), "key");
}
}

参考

bitwiseshiftleft/sjcl: Stanford Javascript Crypto Library (github.com)

JSDoc: Home (bitwiseshiftleft.github.io)

JSDoc: Source: convenience.js (bitwiseshiftleft.github.io)

js 与 java 对接 AES-128-GCM 加密、解密算法 - sjpqy - 博客园 (cnblogs.com)

encryption - How to decrypt of AES-256-GCM created with ruby in sjcl.js - Stack Overflow

encryption - What is right way doing aes gcm decription with sjcl.js? - Stack Overflow

Encrypt AES-GCM in JavaScript, decrypt in Java - Stack Overflow

encryption - Javascript crypto SJCL GCM not decrypting what I encrypted - Stack Overflow

使用 SJCL 实现 AES GCM 加解密 | 肉饼博客 (roubin.me)

AES-GCM 加密简介 - 掘金 (juejin.cn)

https://learn.microsoft.com/zh-cn/dotnet/api/system.security.cryptography.aesgcm

信息加密基础及加密模式 | 一只大菜狗 (cai.dog)

stephenhaunts/Building-Secure-Applications-with-Cryptography-in-.NET-Course-Source-Code: The source code for the Pluralsight course, Building Secure Applications with Cryptography in .NET (github.com)

Potato-Industries/gohide: tunnel port to port traffic over an obfuscated channel with AES-GCM encryption. (github.com)