项目中使用到了AES加密算法,因为要保证终端与服务器加密解密算法一致,并且由于终端形式多样,有C开发的,也有Java/Kotlin开发的,所以需要一套支持前后端的多语言算法,保证通信的安全性和完整性。
AES算法原理的相关文章多如牛毛,本文不再叙述。在实际开发和应用中,AES的算法并不一定是完全按照标准应用的,不同开发人员会对算法进行细微的调整。
Java和C语言基本涵盖了大部分的开发场景,本文就两种语言进行了整合,测试,保证数据的一致性,能够实现Java版本算法加密,C语言版本解密,反之亦可。
本文主要叙述在开发中的应用,简要描述原理
需要代码的请直接到文末下载链接
一、算法描述
本文中算法是AES/CBC/PKCS7Padding,即AES算法,采用CBC工作模式,补码方式采用PKCS7Padding,什么意思呢?
AES是什么
高级加密标准(Advanced Encryption Standard),属于对称加密,就是说加密和解密的过程算法是相反的。AES由DES算法升级而来。
CBC:密码分组链接模式,AES工作模式之一
AES共有五种工作模式:
- 电码本模式(Electronic Codebook Book (ECB));
- 密码分组链接模式(Cipher Block Chaining (CBC));
- 计算器模式(Counter (CTR));
- 密码反馈模式(Cipher FeedBack (CFB));
- 输出反馈模式(Output FeedBack (OFB))
CBC工作模式除了密钥KEY,还需要有初始化向量IV,IV与密钥等长。
PKCS7Padding-填充模式
填充的作用是在加密前将普通文本的长度扩展到需要的长度。关键在于填充的数据能够在解密后正确的移除。
AES有以下几种填充模式:
- NoPadding--顾名思义,就是不填充。缺点就是只能加密长为128bits倍数的信息,一般不会使用
- PKCS#7 & PKCS#5--缺几个字节就填几个缺的字节数。
- ZerosPadding--全部填充0x00,无论缺多少全部填充0x00
- ISO 10126--最后一个字节是填充的字节数(包括最后一字节),其他全部填随机数
- ANSI X9.23--跟ISO 10126很像,只不过ANSI X9.23其他字节填的都是0而不是随机数
二、AES算法原理简述
算法应用模型
image.pngAES算法过程
AES加密过程涉及到4种操作,分别是字节代换、行移位、列混淆和轮密钥加。解密过程分别为对应的逆操作。由于每一步操作都是可逆的,按照相反的顺序进行解密即可恢复明文。加解密中每轮的密钥分别由初始密钥扩展得到。
AES的明文输入被分为16字节的数组,每个16字节进行加密/解密处理。
下面以加密过程为例详细说明,解密过程按步骤反过来即可。
1、字节代换
代换的规则是什么呢?
明文每个字节的高4位作为行值,低4位作为列值,然后取出S盒中对应行列的元素作为输出。字节替换实际就查表和替换操作,AES定义了S盒(加密用的一张表)和逆S盒(解密时用的另外一张表),在加密/解密时,从S盒中找到要替换的字节放到被替换字节的位置上。
例如下面示例代码:
#include <iostream>
using namespace std;
unsigned char sbox[]={
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16,
};
void PrintMatrix(unsigned char m[4][4])
{
for(int i=0;i<4;i++)
{
for(int j=0;j<4;j++)
{
printf("%4x",m[i][j]);
if(j%4==3)
puts("");
}
}
}
void SubByte(unsigned char state[][4])
{
for(int i=0;i<4;i++)
{
for(int j=0;j<4;j++)
{
state[i][j]=sbox[state[i][j]]; //S盒查表操作
}
}
}
int main()
{
unsigned char state[4][4]={
0,4,8,12,
1,5,9,13,
2,6,10,14,
3,7,11,15,
};
printf("明文为:\n");PrintMatrix(state);
SubByte(state); //调用SubByte函数
printf("S盒代换后输出:\n");PrintMatrix(state);
return 0;
}
运行结果:
输入明文为:
0 4 8 12
1 5 9 13
2 6 10 14
3 7 11 15
S盒代换后输出:
63 f2 30 fe
73 6b 1,d7
77 6f 67 ab
7b c5 2b 76
2.行移位变换
行移位变换完成基于行的循环移位操作,变换方法为:第0行不变,第1行循环左移1个字节,第2行循环左移两个字节,第3行循环左移3个字节。
image.png
示例代码:
#include <iostream>
using namespace std;
void PrintfMatrix(unsigned char m[4][4]) //该函数是输出数组方法
{
for(int i=0;i<4;i++)
{
for(int j=0;j<4;j++)
{
printf("%2x",m[i][j]);
if(j%4==3)
puts("");
}
}
}
void ShiftRow(unsigned char state[4][4]) //行移位变换函数
{
unsigned char st[4];
int i,j;
for(i=1;i<4;i++)
{
for(j=0;j<4;j++)
{
st[j]=state[i][(j+i)%4];
}
for(j=0;j<4;j++)
{
state[i][j]=st[j];
}
}
}
int main()
{
unsigned char state[4][4]={
0,4,8,12,
1,5,9,13,
2,6,10,14,
3,7,11,15,
};
printf("明文为:\n");PrintfMatrix(state);
ShiftRow(state); //调用行移位函数
printf("移位后:\n");
PrintfMatrix(state);
return 0;
}
运行结果:
image.png
3.列混淆变换
在AES算法中,需要模多项式m(x)=x8+x4+x^3+x+1。列混合即是用一个常矩阵乘以第二步变换后的矩阵,以达到矩阵中每一个元素都是该元素原所在列所有元素的加权和。
计算示例:
image.png image.png
C语言代码实现:
#include <iostream>
using namespace std;
void PrintfMatrix(unsigned char m[4][4])
{
for(int i=0;i<4;i++)
{
for(int j=0;j<4;j++)
{
printf("%4x",m[i][j]);
if(j%4==3)
puts("");
}
}
}
unsigned char xtime(unsigned char st)
{
return (st<<1)^((st&0x80)?0x1b:0x00); //x乘法 二进制串左移一位,判断最高位是否溢出,溢出要异或0x1b
}
void mixcolumns(unsigned char state[4][4],unsigned char cipher[4][4])
{
for(int j=0;j<4;j++)
{
for(int i=0;i<4;i++)
{
cipher[i][j]=xtime(state[i%4][j]) //0x02乘法
^(state[(i+1)%4][j])^xtime(state[(i+1)%4][j])//0x03乘法
^state[(i+2)%4][j] //0x01乘法
// ^state[(i+2)%4][j]; //0x01乘法 这句写错了,so结果不对,应该是i+3
^state[(i+3)%4][j]; //0x01乘法
}
}
}
int main()
{
unsigned char state[4][4]={
/*0,4,8,12,
1,5,9,13,
2,6,10,14,
3,7,11,15,*/
0x87,0xF2,0x4D,0x97,
0x6E,0x4C,0x90,0xEC,
0x46,0xE7,0x4A,0xC3,
0xA6,0x8C,0xD8,0x95,
};
unsigned char cipher[4][4];
printf("明文为:\n");PrintfMatrix(state);
mixcolumns(state,cipher);
printf("列混合结果:\n");PrintfMatrix(cipher);
return 0;
}
运行结果:
image.png
根据矩阵的乘法可知,在列混淆(利用域GF(28)上的算术特性的一个代替)的过程中,每个字节对应的值只与该列的4个值有关系。此处的乘法和加法需要注意如下几点:
-
(1)将某个字节所对应的值乘以2,其结果就是将该值的二进制位左移一位,如果该值的最高位为1(表示该数值不小于128),则还需要将移位后的结果异或00011011
-
(2)乘法对加法满足分配率,例如:07·S0,0=(01⊕02⊕04)·S0,0= S0,0⊕(02·S0,0)(04·S0,0)
-
(3)此处的矩阵乘法与一般意义上矩阵的乘法有所不同,各个值在相加时使用的是模2加法(异或运算)。
因为:说明两个矩阵互逆,经过一次逆向列混淆后即可恢复原文。
image.png
如下图:
4.轮密钥加
轮密钥加:加密过程中,每轮的输入与轮密钥异或一次(当前分组和扩展密钥的一部分进行按位异或);因为二进制数连续异或一个数结果是不变的,所以在解密时再异或上该轮的密钥即可恢复输入。首尾使用轮密钥加的理由:若将其他不需要密钥的阶段放在首尾,在不用密钥的情况下就能完成逆过程,这就降低了算法的安全性。
加密原理:轮密钥加本身不难被破解,另外三个阶段分别提供了混淆和非线性功能。可是字节替换、行移位、列混淆阶段没有涉及密钥,就它们自身而言,并没有提供算法的安全性。但该算法经历一个分组的异或加密(轮密钥加),再对该分组混淆扩散(其他三个阶段),再接着又是异或加密,如此交替进行,这种方式非常有效非常安全。
算法动画演示
在线演示
云盘下载链接
提取码:sqpe
三、算法代码实现
1、AES算法代码实现在Java版本中使用的是JDK自带的算法,描述了如何使用JDK的算法。
2、C语言版本是完整的算法代码,相较于引用openssl等体积小,移植也较为方便
3、针对经常涉及的Andriod开发,也给出了Java/Kotlin调用方法以及C语言版本的JNI调用示例
Java版本实现AES算法
Java/Kotlin版本在Java开发和安卓开发中只有Base64的编码和解码API有些差别,其它是相同的。
Java实现
public class AesUtil {
private static String AES_MODE = "AES/CBC/PKCS7Padding";
private static String CIPHER = "AES";
private static String CHARSET = "UTF-8";
private static byte[] IV_BYTES = "efghefghefghefgh".getBytes();
private static SecretKeySpec generateKey(byte[] password) {
return new SecretKeySpec(password, CIPHER);
}
public static String encrypt(byte[] password, String message) throws Exception {
try {
SecretKeySpec key = generateKey(password);
byte[] cipherText = encrypt(key, IV_BYTES, message.getBytes());
return new String(Base64.encode(cipherText, Base64.NO_WRAP));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static byte[] encrypt(SecretKeySpec key, byte[] iv, byte[] message) throws Exception {
Cipher cipher = Cipher.getInstance(AES_MODE, "BC");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
return cipher.doFinal(message);
}
public static String decrypt(byte[] password,String base64EncodedCipherText) throws Exception{
try {
SecretKeySpec key = generateKey(password);
byte[] decodedCipherText = Base64.decode(base64EncodedCipherText.getBytes(), Base64.NO_WRAP);
byte[] decryptedBytes = decrypt(key, IV_BYTES, decodedCipherText);
return new String(decryptedBytes);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static byte[] decrypt(SecretKeySpec key,byte[] iv,byte[] decodedCipherText) throws Exception{
Cipher cipher = Cipher.getInstance(AES_MODE);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
return cipher.doFinal(decodedCipherText);
}
}
//测试
public void testAESJava() throws Exception {
System.out.println("测试AES Java版本");
byte[] password = {'a', 'b', 'c', 'd',
'a', 'b', 'c', 'd',
'a', 'b', 'c', 'd',
'a', 'b', 'c', 'd',};
System.out.println("密码字符串形式:" + new String(password));
String encrypt = AesUtil.encrypt(password, "[35380100360174,89860321249940031491,202109301322170001,DEVICE_LOGIN,3,20210930132217,12,8@1@1@0@1@21]");
System.out.println("AES加密结果:" + encrypt);
String decrypt = AesUtil.decrypt(password, encrypt);
System.out.println("AES解密结果:" + decrypt);
}
Kotlin版本
object AesCryptUtil {
private const val AES_MODE = "AES/CBC/PKCS7Padding"
private const val CHARSET = "UTF-8"
private const val CIPHER = "AES"
private const val HASH_ALGORITHM = "SHA-256"
private val IV_BYTES = byteArrayOf('e'.toByte(), 'f'.toByte(), 'g'.toByte(), 'h'.toByte(),
'e'.toByte(), 'f'.toByte(), 'g'.toByte(), 'h'.toByte(),
'e'.toByte(), 'f'.toByte(), 'g'.toByte(), 'h'.toByte(),
'e'.toByte(), 'f'.toByte(), 'g'.toByte(), 'h'.toByte()
)
@Throws(NoSuchAlgorithmException::class, UnsupportedEncodingException::class)
private fun generateKey(password: ByteArray): SecretKeySpec {
return SecretKeySpec(password, CIPHER)
}
@Throws(GeneralSecurityException::class)
fun encrypt(password: String, message: String): String {
try {
val key = generateKey(password)
val cipherText = encrypt(key, IV_BYTES, message.toByteArray(charset(CHARSET)))
//NO_WRAP is important as was getting \n at the end
return Base64Utils.encodeToString(cipherText)
} catch (e: UnsupportedEncodingException) {
throw GeneralSecurityException(e)
}
}
@Throws(GeneralSecurityException::class)
fun encrypt(password: ByteArray, message: String): String {
try {
val key = generateKey(password)
val cipherText = encrypt(key, IV_BYTES, message.toByteArray())
// DDLog.info("cipherText=${cipherText.contentToString()}")
for (b in cipherText) {
val st = String.format("%02X", b)
}
//NO_WRAP is important as was getting \n at the end
return String(Base64Utils.encode(cipherText))
} catch (e: UnsupportedEncodingException) {
throw GeneralSecurityException(e)
}
}
@Throws(GeneralSecurityException::class)
fun encrypt(key: SecretKeySpec, iv: ByteArray, message: ByteArray): ByteArray {
val cipher = Cipher.getInstance(AES_MODE, "BC")
val ivSpec = IvParameterSpec(iv)
cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec)
return cipher.doFinal(message)
}
@Throws(GeneralSecurityException::class)
fun decrypt(password: String, base64EncodedCipherText: String): String {
try {
val key = generateKey(password)
val decodedCipherText = Base64Utils.decode(base64EncodedCipherText.toByteArray())
val decryptedBytes = decrypt(key, IV_BYTES, decodedCipherText)
return String(decryptedBytes, charset(CHARSET))
} catch (e: UnsupportedEncodingException) {
throw GeneralSecurityException(e)
}
}
@Throws(GeneralSecurityException::class)
fun decrypt(password: ByteArray, base64EncodedCipherText: String): String {
try {
val key = generateKey(password)
val decodedCipherText = Base64Utils.decode(base64EncodedCipherText.toByteArray())
val decryptedBytes = decrypt(key, IV_BYTES, decodedCipherText)
return String(decryptedBytes, charset(CHARSET))
} catch (e: UnsupportedEncodingException) {
throw GeneralSecurityException(e)
}
}
@Throws(GeneralSecurityException::class)
fun decrypt(key: SecretKeySpec, iv: ByteArray, decodedCipherText: ByteArray): ByteArray {
val cipher = Cipher.getInstance(AES_MODE)
val ivSpec = IvParameterSpec(iv)
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec)
return cipher.doFinal(decodedCipherText)
}
}
C/C++语言实现AES算法
C语言实现的AES算法,在windows的实现,更多代码请下载:
int testAESCBC_Smartcard() {
std::string skey = "abcdabcdabcdabcd";
std::string siv = "efghefghefghefgh";
std::string pwd = "[35380100360174,89860321249940031491,202109301322170001,DEVICE_LOGIN,3,20210930132217,12,8@1@1@0@1@21]";
std::string encryptedText = aes::encrypt_cbc(pwd, skey, siv);
std::cout << encryptedText << std::endl;
pwd = aes::decrypt_cbc(encryptedText, skey, siv);
std::cout << pwd << std::endl;
return 0;
}
C语言版本的JNI实现:
public class AESHelper {
static {
System.loadLibrary("FlyNative");
}
public native static String helloJNI();
public native static String encodeAESCBC(String key, String iv, String message);
public native static String decodeAESCBC(String key, String iv, String encodedText);
}
JNIEXPORT jstring JNICALL Java_com_flyscale_testaes_AESHelper_encodeAESCBC
(JNIEnv *jniEnv, jclass clazz, jstring key, jstring iv, jstring msg){
std::string encodedText = AESUtil::encryptAESCBC(JavaStringToString(jniEnv, key),
JavaStringToString(jniEnv, iv),
JavaStringToString(jniEnv, msg));
return StringToJavaString(jniEnv, encodedText);
}
JNIEXPORT jstring JNICALL Java_com_flyscale_testaes_AESHelper_decodeAESCBC
(JNIEnv *jniEnv, jclass clazz, jstring key, jstring iv, jstring msg){
std::string decodedText = AESUtil::decryptAESCBC(JavaStringToString(jniEnv, key),
JavaStringToString(jniEnv, iv),
JavaStringToString(jniEnv, msg));
return StringToJavaString(jniEnv, decodedText);
}
网友评论