C言語でAESの暗号化・復号化を、opensslとmcryptで作ってみた
自分用のメモ的な内容ですが、意外とC言語の情報が少ないので、書いておきます。必要に迫られ、AESの暗号化・復号化をC言語で作りました。
AESとは、Advanced Encryption Standardの略で、それまで標準的に使われてきたDES(Data Encryption Standard)の暗号強度が心配になってきたため、新たに規格化された暗号の規格です。パスワードなどの情報の暗号化はもちろん、データや通信経路の暗号化などで幅広く使われています。
暗号化・復号化のプログラムは、自分でゼロから作ってももちろんかまわないのですが、一般的には公開されているライブラリを使用することが多く、よく使われているのは、OpenSSLのライブラリ(libssl,libcrypto)や、PHPで使われることが多いmcryptのライブラリ(libmcrypt)です。ライブラリ自体はちょっと検索すればすぐに見つけてダウンロードできるのですが、どうやって使うのかは意外と調べるのが大変で、特にmcryptはほとんどPHPの情報しか出てきません。C言語派の私としては、意地でもC言語でやりたかったので、試行錯誤してみました。
サンプルソースを載せておきます。どちらも動作モードはECB(Electronic CodeBook)モードです。
OpenSSL
暗号化:aesencrypt.c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <openssl/evp.h>
#include <openssl/aes.h>
int main(int argc,char *argv[])
{
EVP_CIPHER_CTX en;
char *key,*data;
int datasize;
int i,c_len,f_len=0;
unsigned char *ciphertext;
if(argc<=2){
fprintf(stderr,"aesencrypt key data\n");
return(-1);
}
key=argv[1];
if(strlen(key)!=EVP_CIPHER_key_length(EVP_aes_128_ecb())){
fprintf(stderr,"key length must be %d\n",EVP_CIPHER_key_length(EVP_aes_128_ecb()));
return(-1);
}
data=argv[2];
datasize=strlen(data);
EVP_CIPHER_CTX_init(&en);
EVP_EncryptInit_ex(&en,EVP_aes_128_ecb(),NULL,(unsigned char *)key,NULL);
c_len=datasize+EVP_MAX_BLOCK_LENGTH;
ciphertext=calloc(c_len,sizeof(char));
EVP_EncryptUpdate(&en,(unsigned char *)ciphertext,&c_len,(unsigned char *)data,datasize);
EVP_EncryptFinal_ex(&en,(unsigned char *)(ciphertext+c_len),&f_len);
for(i=0;i<c_len+f_len;i++){
printf("%02x",ciphertext[i]);
}
putchar('\n');
free(ciphertext);
EVP_CIPHER_CTX_cleanup(&en);
return(0);
}
復号化:aesdecrypt.c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <openssl/evp.h>
#include <openssl/aes.h>
int main(int argc, char *argv[])
{
EVP_CIPHER_CTX de;
char *key,*data,*ptr;
char buf[80];
int datasize,c;
int p_len,f_len=0;
char *plaintext;
if(argc<=2){
fprintf(stderr,"aesdecrypt key data\n");
return(-1);
}
key=argv[1];
if(strlen(key)!=EVP_CIPHER_key_length(EVP_aes_128_ecb())){
fprintf(stderr,"key length must be %d\n",EVP_CIPHER_key_length(EVP_aes_128_ecb()));
return(-1);
}
data=malloc(strlen(argv[2]));
for(ptr=argv[2],c=0;*ptr!='\0';ptr+=2,c++){
buf[0]=*ptr;
buf[1]=*(ptr+1);
buf[2]='\0';
data[c]=strtol(buf,NULL,16);
}
datasize=c;
EVP_CIPHER_CTX_init(&de);
EVP_DecryptInit_ex(&de,EVP_aes_128_ecb(),NULL,(unsigned char *)key,NULL);
p_len=datasize;
plaintext=calloc(p_len+1,sizeof(char));
EVP_DecryptUpdate(&de,(unsigned char *)plaintext,&p_len,(unsigned char *)data,datasize);
EVP_DecryptFinal_ex(&de,(unsigned char *)(plaintext+p_len),&f_len);
plaintext[p_len+f_len]='\0';
printf("[%s]\n",plaintext);
EVP_CIPHER_CTX_cleanup(&de);
free(plaintext);
free(data);
return(0);
}
mcrypt
暗号化:aesencrypt.c
#include <stdio.h>
#include <stdlib.h>
#include <mcrypt.h>
#include <string.h>
#define MAX_BLOCK_LENGTH 32
int main(int argc,char *argv[])
{
MCRYPT td;
char *key,pad;
unsigned char *data;
int ret,datasize,i,lest,blocksize;
if(argc<=2){
fprintf(stderr,"aesencrypt key data\n");
return(-1);
}
key=argv[1];
fprintf(stderr,"%d\n",mcrypt_module_get_algo_key_size(MCRYPT_RIJNDAEL_128,MCRYPT_ECB)); // なぜか32になる・・・
data=(unsigned char*)calloc(strlen(argv[2])+MAX_BLOCK_LENGTH,sizeof(char));
memcpy(data,argv[2],strlen(argv[2]));
datasize=strlen(argv[2]);
blocksize=mcrypt_module_get_algo_block_size(MCRYPT_RIJNDAEL_128,MCRYPT_ECB);
if((lest=datasize%blocksize)!=0){
pad=blocksize-lest;
for(i=0;i<pad;i++){
data[datasize+i]=pad;
}
datasize+=pad;
}
td=mcrypt_module_open(MCRYPT_RIJNDAEL_128,NULL,MCRYPT_ECB,NULL);
if(td==MCRYPT_FAILED){
return(-1);
}
ret=mcrypt_generic_init(td,key,strlen(key),NULL);
if(ret<0){
mcrypt_perror(ret);
return(-1);
}
mcrypt_generic(td,data,datasize);
for(i=0;i<datasize;i++){
printf("%02x",data[i]);
}
putchar('\n');
mcrypt_generic_deinit(td);
mcrypt_module_close(td);
free(data);
return(0);
}
復号化:aesdecrypt.c
#include <stdio.h>
#include <stdlib.h>
#include <mcrypt.h>
#include <string.h>
int main(int argc,char *argv[])
{
MCRYPT td;
char *key,*data,*ptr;
char buf[80],pad;
int ret,datasize,c,i,bad;
if(argc<=2){
fprintf(stderr,"aesdecrypt key data\n");
return(-1);
}
key=argv[1];
data=malloc(strlen(argv[2]));
for(ptr=argv[2],c=0;*ptr!='\0';ptr+=2,c++){
buf[0]=*ptr;
buf[1]=*(ptr+1);
buf[2]='\0';
data[c]=strtol(buf,NULL,16);
}
datasize=c;
td=mcrypt_module_open(MCRYPT_RIJNDAEL_128,NULL,MCRYPT_ECB,NULL);
if(td==MCRYPT_FAILED){
return(-1);
}
ret=mcrypt_generic_init(td,key,strlen(key),NULL);
if(ret<0){
mcrypt_perror(ret);
return(-1);
}
mdecrypt_generic(td,data,datasize);
pad=data[datasize-1];
if(pad<=datasize){
bad=0;
for(i=datasize-pad;i<datasize;i++){
if(data[i]!=pad){
bad=1;
break;
}
}
if(bad==0){
data[datasize-pad]='\0';
}
}
printf("[%s]\n",data);
mcrypt_generic_deinit(td);
mcrypt_module_close(td);
free(data);
return(0);
}
ビルド時に、
・OpenSSLは-lcryptoを指定
・mcryptは-lmcryptを指定
します。
使い方
# ./aesencrypt 1234567890123456 "This is a test."
86126024747d5d36d3ecdc366f71f15d
#./aesdecrypt 1234567890123456 86126024747d5d36d3ecdc366f71f15d
[This is a test.]
mcryptの方がソースがシンプルになりますが、暗号化の時に16バイト単位にパディングする処理を自分で行わないと、他と互換性がなくなるようです。復号する際にもパディングを外さなければなりません。パディングなしで暗号化すると、OpenSSLの復号化の方で正しく復号できませんでした。パディングの値は、ゼロにする場合や、PKCS#5という、足りない長さの値で埋める方法があるようです。元のデータが文字列ならゼロで埋めれば復号した際にちょうど文字列が切れるので良い感じですが、バイナリデータだと困りますので、PKCS#5が良いでしょう。OpenSSLではゼロで埋めてもPKCS#5でも復号できましたが、JAVAはPKCS#5でないと駄目という情報もありました。JAVAの文字列は'¥0'ターミネートではなかったかと思います。上のサンプルソースではPKCS#5でパディングしています。OpenSSLはライブラリ側でPKCS#5でパディング処理を行ってくれます。なお、PHPだとデフォルトでゼロでパディングするようで、JAVAだと正しく復号できないので、自分で先にPKCS#5でパディングしてから暗号化すると良いようです。
PHPを使う人であればmcryptの方が同じ感じに記述できて良いかも知れませんが、ライブラリが入っている可能性からするとOpenSSLの方が入っている確率が高い気がするので、C言語ならOpenSSLのライブラリの方が、いつでも使える可能性が高いかも知れません。ダイナミックリンクすると、共有ライブラリが存在しないと実行時にエラーになりますので、個人的にはどこにでも入っている可能性が高い方が好きです。
とてもマニアックな話題でした。。
==追記==
少しソースを修正しました。OpenSSLのソースの方は鍵の長さが正しくない場合(EVP_CIPHER_key_length(EVP_aes_128_ecb()):16バイト)に、エラーにするようにしました。OpenSSLのライブラリを使うと、16バイト以外でも暗号化できてしまうのですが、復号がうまくできないみたいです。同様な修正をmcryptの方にもしようとしたのですが、mcrypt_module_get_algo_key_size(MCRYPT_RIJNDAEL_128,MCRYPT_ECB)が32バイトを返すため、綺麗なソースにできませんでした。。16バイト固定でチェックすることはもちろんできますが。128(=16x8)と指定しているので、16バイトだと思うのですけどねぇ。。