高级加密标准(英语:Advanced Encryption Standard,缩写:AES),在密码学中又称Rijndael(发音近于 “Rhine doll”)加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。2006年,高级加密标准已然成为对称密钥加密中最流行的算法之一。
以上修改自wiki
对于这个算法的底层的细节我也没能力研究,不过框架可以用一个图来说明:
因为这里主要不是讲这个算法本身,所以就没啥理论的东西了,现就遇到的问题总结下
存储形式
原文/密文/密钥 都是unsigned char类型(hex),很多时候这些都不是hex类型,因为很多应用需要以文本的形式传输,通常有两种”文本”类型:
- 按照hex字节码存放,如,
0x0a771bcf
——>0A771BCF
- 转为base64,如,
0x0a771bcf
——>Cncbzw==
真实密钥
以openssl为例
echo "helloworld" | openssl enc -aes-128-cbc -nosalt -pass pass:password
得到的结果为:
0000000: fa7d d88c 8f9d d990 ffac 557a 69ae 0a82
注意这里的pass字段,很多实现都是限制密码长度为16bytes(=128bits)的,但是这里却没有限制,其实,这里的pass并不是真正的密钥,真正的密钥是key,而且还要结合初始化向量iv,这两个会由pass生成:
[root@localhost AES-C]$ openssl enc -aes-128-cbc -nosalt -pass pass:passwordpassword -P
key=9DBB300E28BC21C8DAB41B01883918EB
iv =6E63301487DFD4F0EE00A3D12D502D30
当然openssl可以只指定key和iv,而不需要pass,pass生成key和iv的例子程序如下(无salt版):
//gcc -lcrypto
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
int main(int argc, char *argv[])
{
const EVP_CIPHER *cipher;
const EVP_MD *dgst = NULL;
unsigned char key[EVP_MAX_KEY_LENGTH], iv[EVP_MAX_IV_LENGTH];
const char *password = "passwordpassword";
const unsigned char *salt = NULL;
int i;
OpenSSL_add_all_algorithms();
cipher = EVP_get_cipherbyname("aes-128-cbc");
if(!cipher) { fprintf(stderr, "no such cipher\n"); return 1; }
dgst=EVP_get_digestbyname("md5");
if(!dgst) { fprintf(stderr, "no such digest\n"); return 1; }
if(!EVP_BytesToKey(cipher, dgst, salt,
(unsigned char *) password,
strlen(password), 1, key, iv)){
fprintf(stderr, "EVP_BytesToKey failed\n");
return 1;
}
printf("Key: "); for(i=0; i<cipher->key_len; ++i) { printf("%02x", key[i]); } printf("\n");
printf("IV: "); for(i=0; i<cipher->iv_len; ++i) { printf("%02x", iv[i]); } printf("\n");
return 0;
}
IV及padding
默认情况下,AES的IV为全零,padding为PKCS5Padding,如果在不同库之间进行加解密,可能不一样,比如mcrypt就是NoPadding,而javax.crypto.Cipher和mcrypt之间交互就需要指定为Cipher.getInstance("AES/CBC/NoPadding")
这里是Cipher(java)和mcrypt(c)加解密的例子
特殊行为 有的库不知道出于什么原因,没有按照正常的规则实现AES,比如gwt-crypto,当需要在GWT的client端实现加密,C写的客户端解密,由于GWT本身的限制,只能用此库,可是它的行为很诡异,生成的密文和其它库实现的都不一样,C当然解不开了…… 调试的过程中发现doFinal上有这样一句注释:
Process the last block in the buffer. If the buffer is currently full and padding needs to be added a call to doFinal will produce 2 * getBlockSize() bytes.
而且,代码里还有个这个过程(这里的cbcV为iv):
/*
* XOR the cbcV and the input,
* then encrypt the cbcV
*/
for (int i = 0; i < blockSize; i++)
{
cbcV[i] ^= in[inOff + i];
}
这种情况基本打消了用现有c库实现解密的念头,于是找了个openssl剥离出来的c自己改了,下面是一个主要文件,其它见github:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "ex_aes_decrypt.h"
int char2int(char ch)
{
if(ch >= 'a' && ch <= 'z') return ch - 'a' + 10;
else return ch - '0';
}
void hex2bin(char *hex, int len, u8 *bin)
{
int i;
for(i = 0; i < len; ++i){
bin[i] = (u8)((char2int(hex[i*2])<<4) + char2int(hex[i*2 + 1]));
}
}
void char2bin(char *ch, int len, u8 *bin)
{
int i;
for(i = 0; i < len; ++i){
bin[i] = (u8)ch[i];
}
}
void bin2char(char *ch, int len, u8 *bin)
{
int i;
for(i = 0; i < len; ++i){
ch[i] = (char)bin[i];
}
}
int getPandding(u8 *bin)
{
int index = 0;
while(index < AES_BLOCK_SIZE){
int i, flag = 0;
for(i = index; i < AES_BLOCK_SIZE; ++i){
if(AES_BLOCK_SIZE - index != bin[i]){
flag = 1;
break;
}
}
if(flag == 0) return index;
++index;
}
return index;
}
/*
* By Cody Chan<int64ago@gmail.com>
* iv[] = {0}
* PKCS5Padding
* key must be 16 bytes
* in_len%16 == 0
* return: out
*/
char *ex_aes_decrypt(char *in, int in_len, char *key_)
{
in_len /= 2;
AES_KEY key;
u8 key_bin[AES_BLOCK_SIZE];
u8 *in_bin = (u8*)malloc(sizeof(u8)*in_len);
u8 *out_bin = (u8*)malloc(sizeof(u8)*in_len + 1);
char *out = (char *)malloc(sizeof(char)*in_len + 1);
char iv[AES_BLOCK_SIZE] = {0};
hex2bin(in, in_len, in_bin);
char2bin(key_, AES_BLOCK_SIZE, key_bin);
AES_set_decrypt_key(key_bin, 128, &key);
int blocks = in_len / AES_BLOCK_SIZE;
int i;
for(i = 0; i < blocks; ++i){
AES_decrypt(in_bin + i*AES_BLOCK_SIZE,
out_bin + i*AES_BLOCK_SIZE, &key);
int j;
for(j = 0; j < AES_BLOCK_SIZE; ++j){
out_bin[i*AES_BLOCK_SIZE + j] ^= iv[j];
iv[j] = in_bin[i*AES_BLOCK_SIZE + j];
}
}
int padding = getPandding(out_bin + (blocks-1)*AES_BLOCK_SIZE);
out_bin[(blocks-1)*AES_BLOCK_SIZE + padding] = '\0';
bin2char(out, in_len, out_bin);
free(in_bin);
free(out_bin);
return out;
}
Tips
当然上面的注意事项只有在跨库使用的时候才可能会发生,如果用的同一个库,一般都有接口非常统一的encrypt和decrypt,放心用即可