最近在做的项目,需要账户密码登录,采用 AES 加密算法,从认识到了解,以及后续的扩展学习。
首先,明确几个加解密的重要概念
密钥
在明文转换为密文或将密文转换为明文的算法中输入的参数。密钥分为对称密钥(私用密钥)与非对称密钥(公共密钥)。
密钥加密
发送和接收数据的双方,使用相同的或对称的密钥对明文进行加密解密运算的加密方法。
加密向量
通常采用密钥长度的随机文本块对纯文本进行异或运算,然后再对其进行加密,产生一个加密文本块。然后,将前面产生的密文块作为一个初始化向量对下一个纯文本块进行异或运算。
对称加密算法中,如果只有一个密钥来加密数据的话,明文中的相同文字就会也会被加密成相同的密文,这样密文和明文就有完全相同的结构,容易破解。如果给一个初始化向量,第一个明文使用初始化向量混合并加密,第二个明文用第一个明文的加密后的密文与第二个明文混合加密,这样加密出来的密文的结构则完全与明文不同,更加安全可靠。
综上,加密之所以安全,并非不知道加解密算法,而是加密的密钥和向量是绝对的隐藏,加密向量可以增加加密算法的强度(区块加密)。
AES
那么,什么是 AES 加密呢?
Advanced Encryption Standard(高级加密标准),在密码学中又叫 Rijndael 加密法,是美国联邦政府采用的一种区块加密标准。
- 替代 DES
- 对称密钥加密(迭代对称分组密码体制)
- 代换-置换网络(基于排列和置换运算),非Feistel架构(DES)
- AES 加密数据块分组长度必须为128比特,密钥长度可以是128比特、192比特、256比特中的任意一个
Rijndael 密码的设计力求满足以下3条标准:
当前的大多数分组密码,其轮函数是Feistel结构。Rijndael 轮函数是由3个不同的可逆均匀变换组成。
具体信息参见:AES是个什么鬼?
本文后续代码基础:AES加密,密钥位数不足转换成byte[]后填充(byte)0,加密向量16位,加密模式CBC,填充模式PKCS5Padding,字符集是UTF-8,输出是HEX格式
其中,PKCS5Padding(Java)与 PKCS7(C#)是可以互相加解密的,这是正好匹配的情况。
但是,经常会存在合作双方平台不一致、跨语言接口对接导致数据不一致的问题,可以采用ikvm工具将.jar包转换为.dll,则在.Net平台直接引用并调用方法即可
/// <summary>
/// AES加密
/// </summary>
/// <param name="_pwd">明文密码</param>
/// <param name="_key">加密密钥</param>
/// <param name="_iv">加密向量</param>
/// <returns></returns>
public static string AESEncrypt(string _pwd, string _key, string _iv, int _keyLen, int _ivLen)
{
byte[] toEncryptArray = UTF8Encoding.UTF8.GetBytes(_pwd);
using (AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider())
{
aesProvider.Mode = System.Security.Cryptography.CipherMode.CBC;
aesProvider.Padding = System.Security.Cryptography.PaddingMode.PKCS7;
aesProvider.Key = GetAesKey(_key, _keyLen);
aesProvider.IV = GetAesVector(_iv, _ivLen);
using (MemoryStream ms = new MemoryStream())
{
using (CryptoStream cs = new CryptoStream(
ms, aesProvider.CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(toEncryptArray, 0, toEncryptArray.Length);
cs.FlushFinalBlock();
cs.Close();
}
string resultStr = ByteArrayToHexString(ms.ToArray()); // Convert.ToBase64String
ms.Close();
return resultStr;
}
}
}
/// <summary>
/// AES解密
/// </summary>
/// <param name="_pwd">暗文密码</param>
/// <param name="_key">密钥</param>
/// <param name="_iv">向量</param>
/// <returns></returns>
public static string AESDecrypt(string _pwd, string _key, string _iv, int _keyLen, int _ivLen)
{
byte[] toDecryptArray = HexStringToByteArray(_pwd);
using (AesCryptoServiceProvider aesProvider = new AesCryptoServiceProvider())
{
aesProvider.Mode = System.Security.Cryptography.CipherMode.CBC;
aesProvider.Padding = System.Security.Cryptography.PaddingMode.PKCS7;
aesProvider.Key = GetAesKey(_key, _keyLen);
aesProvider.IV = GetAesVector(_iv, _ivLen);
using (MemoryStream ms = new MemoryStream(toDecryptArray))
{
byte[] decryptBytes = new byte[toDecryptArray.Length];
string resultStr = string.Empty;
using (CryptoStream cs = new CryptoStream(
ms, aesProvider.CreateDecryptor(), CryptoStreamMode.Read))
{
if (cs.Read(decryptBytes, 0, decryptBytes.Length) > 0)
{
resultStr = Encoding.UTF8.GetString(decryptBytes);
}
cs.Close();
}
ms.Close();
return resultStr;
}
}
}
/// </summary>
/// 获取AES密钥
/// </summary>
/// <param name="_key">Aes密钥字符串</param>
/// <param name="_length">默认长度16字节</param>
/// <returns>Aes密钥</returns>
public static byte[] GetAesKey(string _key, int _length)
{
byte[] resBytes = new byte[_length];
byte[] keyBytes = UTF8Encoding.UTF8.GetBytes(_key);
int lenTmp = keyBytes.Length;
if (lenTmp <= _length)
{
Array.Copy(keyBytes, resBytes, lenTmp);
}
else
{
Array.Copy(keyBytes, resBytes, _length);
}
return resBytes;
}
/// <summary>
/// 获取AES向量
/// </summary>
/// <param name="_iv">Aes向量字符串</param>
/// <param name="_length">默认长度16字节</param>
/// <returns>Aes向量</returns>
public static byte[] GetAesVector(string _iv, int _length)
{
byte[] resBytes = new byte[_length];
if (string.IsNullOrWhiteSpace(_iv))
{
// _iv为空,返回全0字节数组
return resBytes;
}
else
{
byte[] ivBytes = UTF8Encoding.UTF8.GetBytes(_iv);
int lenTmp = ivBytes.Length;
if (lenTmp <= _length)
{
Array.Copy(ivBytes, resBytes, lenTmp);
}
else
{
Array.Copy(ivBytes, resBytes, _length);
}
return resBytes;
}
}
/// <summary>
/// 将一个byte数组转换成一个格式化的16进制字符串
/// </summary>
/// <param name="data">byte数组</param>
/// <returns>格式化的16进制字符串</returns>
public static string ByteArrayToHexString(byte[] data)
{
StringBuilder sb = new StringBuilder(data.Length * 3);
foreach (byte b in data)
{
//16进制数字
sb.Append(Convert.ToString(b, 16).PadLeft(2, '0'));
}
return sb.ToString().ToUpper();
}
/// <summary>
/// 将一个格式化的16进制字符串转换成一个byte数组
/// </summary>
/// <param name="?"></param>
/// <returns></returns>
public static byte[] HexStringToByteArray(string str)
{
str = str.Replace(" ", "");
byte[] buffer = new byte[str.Length / 2];
for (int i = 0; i < str.Length; i += 2)
{
buffer[i / 2] = (byte)Convert.ToByte(str.Substring(i, 2), 16);
}
return buffer;
}
测试发现一个解密的bug,会出现解密结果 "288008 " 的情况,遂推荐如下方法
using (ICryptoTransform transform = aesProvider.CreateDecryptor())
{
byte[] plainText = transform.TransformFinalBlock(toDecryptArray, 0, toDecryptArray.Length);
return Encoding.UTF8.GetString(plainText);
}
或直接在返回时进行替换处理: return resultStr.Replace("