PHP的加密函数库Mcrypt使用介绍

翻译文章,原文链接:
 
PHP Cryptography: An Introduction Using Mcrypt
作者:Robert Peake
翻译:ericfish


此文介绍双向加密算法,解释它于其它常用PHP函数的不同,比如md5和rot13,以及如何在单向哈希算法和双向加密之间做出正确的选择。同时介绍作为可动态加载php插件mcrypt的安装。文中将以加密cookies和数据库信息(如信用卡卡号)作为应用实例,以脚本语言的形式给出加密/解密方案,并提供编码、源码编译命令和创建新的插件的解决方案。


为什么要使用密码?
The human heart has hidden treasures,
In secret kept, in silence sealed.
-Charlotte Bront?, Evening Solace


人的本性中有着隐藏的欲望,从计算的角度来讲,这就是密码学。


密码学定义
英文牛津字典里的描述是:一种秘密的写作习惯…只有那些拥有密钥的人才能理解。
从现代定义应该是:用数学的方法将一种形式转换成另一种新的形式(加密),然后可以将新的形式转换回原来的形式(解密)。


比较
Scheme  Direction  Security        Results  Key?   Type 
md5        one-way    secure         same      no       hashing 
rot13       two-way     not secure  same      no       mapping 
mcrypt    two-way     secure         different  yes     encryption


首先必须指出的是mcrypt是一个双向加密算法,虽然它也有可以生成单向的哈希,但我们的主要用途还是能够同时加密和解密的能力。Md5则是一个单向的加密算法。
一个最简单的单向算法,比如把每个字符的ASCII码求和。示例如下:


Listing 1: A simple one-way mapping (one_way.php)


<?php


//return an ordinal summation
function one_way ($string){
for ($i=0; $i < strlen($string); $i++) {
$result += ord(substr($string,$i));
}
return $result;
}


$test_1 = “Hello, world.”;
$test_2 = “(%PHP Int’l Mag%)”;
$test_3 = “foo”;


print $test_1;
print ‘ = ‘;
print one_way($test_1);
print ‘<br />’;


print $test_2;
print ‘ = ‘;
print one_way($test_2);
print ‘<br />’;


print $test_3;
print ‘ = ‘;
print one_way($test_3);


?>


Listing 2: Browser Output


Hello, world. = 1174
(%PHP Int’l Mag%) = 1174
foo = 324


正如例子中所显示的,使用单向机密算法所得到的加密的值所对应的原文可能并不唯一。但md5和上面最简单的示例不同,它是一种能够保证输出唯一的算法。
md5的原则是:对每个输入a,它能够生产对应的一个b,但对每一个b而言,要把它在还原成a是很困难的。这就是为什么我们认为用单向算法生成唯一的hashes是安全的。Md5通常被用于创建唯一的文件签名并用来与其它的签名进行比较。
我还见过使用md5来加密用户在数据库中的密码。当用户输入他们的密码后,系统创建对应的md5 hash码,并与系统中存储的md5 hash码进行比较。如果他们相同,那么绝大部分情况下它们所对应的明文也是相同的。在PHP的加密算法中使用来DES来实现这一功能,DES是UNIX中/etc/passwd存储密码所使用的加密标准。这一点有时是很有用的,比如在php与htaccess间创建接口,或与/etc/password本身。


rot13是一种最简单的双向机密算法。该算法的规则是对26个字母分别使用其在字母表中右边第十三个字母替换。


Listing 3: A simple rot13 test (rot13.php)


<?php
$s = ‘abcdefghijklmonpqrstuvwxyz’;
$s_0 = str_rot13($s);


print ‘<code>’;
print $s;
print “<br />n”;
print $s_0;
print ‘</code>’;
?>



Listing 4: Output


abcdefghijklmonpqrstuvwxyz
nopqrstuvwxyzbacdefghijklm



很显然,rot13虽然简单但不安全。rot13一般用于世界性的新闻组网络系统中模糊一些敏感性文字,使它们必须通过解码才能被阅读。
需要指出的是一些更高级的算法比如base_64和rawurlencode 和rot13一样都属于映射的类型。它们都通过表格中一对一的字符映射实现。正因如此它们只能被用于暂时模糊数据。因为在数学上对这些类型的加密进行方向解密是比较容易实现的,所以它们不应该被视为一种安全的方案。
相比而言,mcrypt中提供的算法通过引入密钥的概念正好实现了以上两种算法的优点。一般来说密钥即密码的意思,它是可以由任何字符组成的针对特定算法定长的字符串。通过结合密钥和注入的随机数(称为’IV’),mcrypt加密算法的过程为每个待加密的唯一值a,加密后另外一个不同的值b被生成,这个b值如果要它还原成a,就必须将密钥和与加密时相同的随机数组返回给mcrypt解密算法。这才是真正意义上的加密,也是mcrypt库提供给我们的。


合理的使用
何时使用mcrypt?何时使用哈希或者映射?


如果你只是需要简单的模糊信息,那么映射就可以满足。所谓模糊,也就是说使数据不能被直接阅读。但如果你传输的是敏感数据,则不要用映射,因为数据并没有被真正地加密,它只是用一种可被预知的方式映射字符。
如果你只是需要比较数据,那么可以使用md5。它可以被用来比较文件、密码、甚至比较那些你不希望被别人猜到和复制的唯一键。你还可以使用md5用来作为对安全加密算法的附加安全手段,我们回在下面的文中提到。
如果你有信息被存储在不安全的地方,你的信息将会横穿互联网(如电子邮件),或者其它一些安全需求比较高的信息,那么就需要使用到mcrypt所提供的双向密钥加密算法了。


安装
此安装主要针对Linux系统,但对于UNIX(包括Mac OS X)和Windows系统也同样适用。对于Debian的用户,安装mcrypt也一样简单。


# apt-get install php4-mcrypt
# apacectl restart


我建议将mcrypt做为动态加载的插件安装到PHP中。它将使得PHP更容易维护和升级,因为你不用一直去重新编译。如果你一定要把mcrypt包含到PHP中,在命令行配置时可以用mcrypt=’/path/to/mcrypt’ 来加载。
但无论如何,首先你需要安装libmcrypt. Libmcrypt可以在SourceForge被下载。使用root用户输入:


# wget -c
‘http://path/to/nearest/sourceforge/mirror/
for/latest/libmcrypt’
# gunzip libmcrypt-X.X.X.tar.gz
# tar -xvf libmcrypt-X.X.X.tar
# cd libmcrypt-X.X.X
# ./configure


现在你应该已经把libmcrypt作为一个共享组件了(但不是一个PHP的共享组件)。运行命令:


# ldconfig


它将使得共享对象可以在C/C++开发中被使用。下面把mcrypt动态组件编译到PHP中。首先,你需要PHP-devel 包中包含的 ‘phpize‘ 命令。


当你当前运行的PHP中已经有了 PHP-devel 后,输入:


# cd ext/mcrypt
# phpize
# aclocal
# ./configure
# make clean
# make
# make install


现在/usr/lib/php4 目录下面应该有了mcrypt.so 的文件,在 /etc/php.ini 添加:


extension=mcrypt.so


然后输入命令:


# apachectl restart


这样我们就已经将mcrypt功能安装成功了。



测试Mcrypt
我写了一个嵌套循环的php页面,可以用于检验libmcrypt安装的完整性


Listing 5: A simple loop to test all ciphers and modes (mcrypt_check_sanity.php)


<?php
/* run a self-test through every listed
* cipher and mode
*/
function mcrypt_check_sanity() {
 $modes = mcrypt_list_modes();
 $algorithms = mcrypt_list_algorithms();


 foreach ($algorithms as $cipher) {
  if(mcrypt_module_self_test($cipher)) {
   print $cipher.” ok.<br />n”;
  }
  else {
   print $cipher.” not ok.<br />n”;
  }
  foreach ($modes as $mode) {
   if($mode == ‘stream’) {
    $result = “not tested”;
   }
   else if(mcrypt_test_module_mode($cipher,$mode)) {
    $result = “ok”;
   }
   else {
    $result = “not ok”;
   }
   print $cipher.” in mode”.$mode.” “.$result.”<br />n”;
  }
 }
}


// a variant on the example posted in
// mdecrypt_generic
function mcrypt_test_module_mode($module,$mode) {
 /* Data */
 $key = ‘this is a very long key, even too
 long for the cipher’;
 $plain_text = ‘very important data’;


 /* Open module, and create IV */
 $td = mcrypt_module_open($module,”,$mode, ”);
 $key = substr($key, 0,mcrypt_enc_get_key_size($td));
 $iv_size = mcrypt_enc_get_iv_size($td);
 $iv = mcrypt_create_iv($iv_size,MCRYPT_RAND);


 /* Initialize encryption handle */
 if (mcrypt_generic_init($td, $key, $iv) != -1) {


  /* Encrypt data */
  $c_t = mcrypt_generic($td, $plain_text);
  mcrypt_generic_deinit($td);


  // close the module
  mcrypt_module_close($td);


  /* Reinitialize buffers for decryption*/
  /* Open module, and create IV */
  $td = mcrypt_module_open($module, ”,$mode, ”);
  $key = substr($key, 0,mcrypt_enc_get_key_size($td));


  mcrypt_generic_init($td, $key, $iv);
  $p_t = trim(mdecrypt_generic($td,$c_t)); //trim to remove padding


  /* Clean up */
  mcrypt_generic_deinit($td);
  mcrypt_module_close($td);
 }


 if (strncmp($p_t, $plain_text,strlen($plain_text)) == 0) {
  return TRUE;
 }
 else {
  return FALSE;
 }
}


// function call:
mcrypt_check_sanity();
?>


如果其中的一些算法(英文叫cipher)能够正常工作,那么我们就开始将这些算法应用到实例中了。如果它们还不能工作,那么你还需要花一点时间在php.net上的mcrypt说明。


加密 Cookies
Cookies长久以来一直被打上来间谍软件(spyware)的烙印。这是因为大多数情况下它们在一种可预见的方式下使用。很多流行的站点将用户信息通过明文的方式存储在cookie里。恶意站点可以通过伪装成原站点的手段来获取这些信息。
这种方法还能引起原站点的安全性问题,比如在cookie中设置对该站点进行攻击的数据。
无论是第三方的站点窃取cookie信息,还是恶意用户通过更改cookie来攻击站点,都可以通过加密来避免。一个加密的cookie信息对于第三方是毫无用处的,而且它几乎不可能被篡改。
并不著名的PHP sessions也提供了一种安全存储cookie的方法,可以通过于session关联来保证安全。(However, brute force attacks using the common cookie value ‘PHPSESSID’ would soon defeat this layer of “security.” )更多关于session安全的问题可以阅读Chris Shiflett的文章:”The Truth About Sessions” ( www.php-mag.net/itr/online_artikel/psecom,id,513,nodeid,114.html ).
使用真正的加密算法的好处是你不被限制在使用一个唯一键值上。当你需要把一些重要的信息(如密码)放到cookie中,使用加密是最合适的方法。
最好将密钥的信息存放在服务器一端。
在Listing 6 中我们将用最简单的方法来实现两个方法,使用’ecb’模式的’blowfish’算法。因为ecb模式不需要IV,使得过程能够尽量的简单。


Listing 6: cookie encryption/decryption functions (twofish_cookie.php)


<?php


function my_encrypt($string) {
 $key = ‘supersecret’;
 $key = md5($key);


 /* Open module, trim key to max length */
 $td = mcrypt_module_open(‘twofish’,”,’ecb’, ”);
 $key = substr($key, 0,mcrypt_enc_get_key_size($td));


 /* Initialize encryption handle
 * (use blank IV)
 */
 if (mcrypt_generic_init($td, $key, ”) != -1) {
  /* Encrypt data */
  $c_t = mcrypt_generic($td, $string);
  mcrypt_generic_end($td);
  mcrypt_module_close($td);
  return $c_t;
 } //end if
}


function my_decrypt($string) {
$key = ‘supersecret’;
$key = md5($key);


/* Open module, trim key to max length */
$td = mcrypt_module_open(‘twofish’,”,’ecb’, ”);
$key = substr($key, 0,mcrypt_enc_get_key_size($td));


/* Initialize encryption handle
* (use blank IV)
*/
if (mcrypt_generic_init($td, $key, ”) !=-1) {


/* Encrypt data */
$c_t = mdecrypt_generic($td, $string);
mcrypt_generic_end($td);
mcrypt_module_close($td);
return trim($c_t); //trim to remove
//padding
} //end if
}


function my_encryptcookie($string) {
return base64_encode(my_encrypt($string));
}


function my_decryptcookie($string) {
return my_decrypt(base64_decode($string));
}


?>


注意我们使用了md5来加密字符串”supersecret”来生成密钥。并且算法的输出是定长的,并将不够的长度用空格代替,所以我们需要用trim()去掉这些返回的空格来还原它。
在加密后需要将字节输出转换成字符串,但在把密文传给解密函数前需要把它还原为字节码。这点在设置cookies和用于邮件传输协议数据的时候是很重要的。
接下来我们可以测试输出了:


<?php
include(“twofish_cookie.php”);
print my_decrypt(my_encrypt
(“Hello, world.<br />n”));
print my_decryptcookie
(my_encryptcookie(“Hello, world.<br />n”));
?>


因为加密解密函数的可逆性,我们同样还可以写成:


<?php
include(“twofish_cookie.php”);
print my_encrypt(my_decrypt(“Hello, world.”));
?>


但不能将base_64的函数用到加密中去,否则加密会出现问题:


<?php
include(“twofish_cookie.php”);
print my_encryptcookie(my_decryptcookie(“Hello, world.”));
?>



此时浏览器会输出:


Hello+worlc


因为base64是一个交迭的映射方法,此时base64_decrypt方法被使用在一个已解密的字符串上。因为在加密和解密中映射的相同,所以在rot13中交迭的部分是没有逻辑的,但base64中不同。我们来看一个设置cookie的例子,Listing 7.


Listing 7: set_twofish_cookie.php


<?php
include(“twofish_cookie.php”);
if($_COOKIE[‘test’]) print
my_decryptcookie($_COOKIE[‘test’]);
else {
$s = my_encryptcookie(“Hello, world.”);
if(setcookie(‘test’, $s, time()+3600)) {
print ‘Cookie set.’;
}
}
?>


当页面第一次载入的时候会设置一个加密的名为 ‘test’的cookie,在接下去再次从同一客户端访问时它将会返回解密后的cookie。
现在你已经成功的将一个加密的cookie值保存到了客户端浏览器中,并且成功的在应用中解密了这个信息。这种方法比起用base64或者URL来编码你的信息要更为有效得多,而且它比使用session的安全性要高得多。



加密SQL数据
现在我们来考虑一个更高技得应用。假如你在运营一个涉及信用卡信息得电子商务公司。你需要一个地方去存储这些信息,一般来讲SQL数据库是比较常见得选择。你可能会在每次交易 进行的时候存储这些数据,或者你需要更长时间的存储来实现一些预定的服务。而这些信息在数据库里面的时间越长,它们积累的可能性就高,你的数据库被恶意入侵的可能性也越高。如果所有数据都是用明文的方式存储在数据库中,那么一旦数据库被入侵,所有这些信息马上可以被入侵者所掌握。
另外,随着网上大量使用PHPMyAdmin来管理MySQL数据库,黑客就有了另外一种不涉及系统权限的入侵途径,它们能够在PHPMyAdmin下面建立目录的权限就足够了。如果数据是明文的,黑客只需要用本机浏览器就能够方便的获得这些敏感信息。
那么解决方法是什么呢?双向加密使你能够安全的保存和获得数据,因为当它们在数据库里,它们使加密的,当它们被显示给授权的用户时它们才被解密。(见 Listing 8)


Listing 8: AES Encryption (aes_sql.php)


<?php
function my_encrypt($string) {
 srand((double) microtime() * 1000000);
 //for sake of MCRYPT_RAND


 $key = ‘supersecret’;
 $key = md5($key);


 /* Open module, and create IV */
 $td = mcrypt_module_open(‘rijndael-128′,”,’cfb’, ”);
 $key = substr($key, 0,mcrypt_enc_get_key_size($td));
 $iv_size = mcrypt_enc_get_iv_size($td);
 $iv = mcrypt_create_iv($iv_size,MCRYPT_RAND);


 /* Initialize encryption handle */
 if (mcrypt_generic_init($td, $key, $iv) !=-1) {
  /* Encrypt data */
  $c_t = mcrypt_generic($td, $string);
  mcrypt_generic_deinit($td);
  mcrypt_module_close($td);
  $c_t = $iv.$c_t;
  return $c_t;
 } //end if
}


function my_decrypt($string,$key) {
 $key = md5($key);


 /* Open module, and create IV */
 $td = mcrypt_module_open(‘rijndael-128′,”,’cfb’, ”);
 $key = substr($key, 0,mcrypt_enc_get_key_size($td));
 $iv_size = mcrypt_enc_get_iv_size($td);
 $iv = substr($string,0,$iv_size);
 $string = substr($string,$iv_size);


 /* Initialize encryption handle */
 if (mcrypt_generic_init($td, $key, $iv) !=-1) {
  /* Encrypt data */
  $c_t = mdecrypt_generic($td, $string);
  mcrypt_generic_deinit($td);
  mcrypt_module_close($td);
  return $c_t;
 } //end if
}
?>


这里我们使用cbc模式,该模式包含了IV。所以预先生成我IV,因为它是定长,所以我们在解密前用了substr()函数。接下来是一个简单的测试。


<?php
include(“aes_sql.php”);


$string = “Hello, world.”;
print my_decrypt(my_encrypt($string),’supersecret’);
?>


注意到这里我们没有像cookie方法那样在这里创建包装函数。如果你需要将除了blob类型以外的数据存到数据库中去的时候,你需要像前面定义cookie函数那样定义包装函数,它将保证SQL屈居被正确的执行。
这个例子与前面cookie的例子另一个不同之处在于,这里的解密函数需要将密钥传进去。在网站系统中,可以将这个密钥与系统的密码关联。这种将密码传入解密函数的方法,为你的应用增加了一个安全级别,因为攻击者必须要知道这个密码才行。
然而如果入侵者可以进入到你的系统中,那么发现这个密码应该也不会是一件很困难的事情,因为它以明文方式写在你的PHP脚本之中。


接下去我们将讨论如何用加密的函数来模糊密码,从而进一步提高安全性。
你可以使用一些其它的数据库中的信息,也可以对密钥再进行加密,但这种方式变成了一种无限地循环。
The process of creating an automatic encryption scheme that is impervious to tampering if penetrated is ultimately beyond the scope of this article. We can, however, look at how to introduce some serious roadblocks.
第一种方法是考虑使用PHP encoder。第二种方法是通过编译命令行应用来做这个工作。


Listing 9: C my_crypt command line application


#include <mcrypt.h>
#include <stdio.h>
#include <stdlib.h>
/* #include <mhash.h> */
main( int argc, char *argv[] ) {
return my_crypt();
}


int my_crypt() {
MCRYPT td;
int i;
char *key;
char password[20];
char block_buffer;
char *IV;
int keysize=16; /* 128 bits */
key=calloc(1, keysize);
strcpy(password, “supersecret”);
/* Generate the key using the password */
/*
mhash_keygen( KEYGEN_MCRYPT, MHASH_MD5,
key, keysize, NULL, 0, password,
strlen(password));
*/


memmove( key, password, strlen(password));
td = mcrypt_module_open(“rijndael-128”,
“”, “cfb”, “”);
if (td==MCRYPT_FAILED) {
return 1;
}
IV = malloc(mcrypt_enc_get_iv_size(td));
/* Put random data in IV. Note these are not
* real random data, consider using
* /dev/random or /dev/urandom.
*/
/* srand(time(0)); */
for (i=0; i< mcrypt_enc_get_iv_size( td);
i++) {
IV[i]=rand();
}
i=mcrypt_generic_init( td, key, keysize,
IV);
if (i<0) {
mcrypt_perror(i);
return 1;
}
/* Encryption in CFB is performed in bytes
*/
while ( fread (&block_buffer, 1, 1, stdin)
== 1 ) {
mcrypt_generic (td, &block_buffer, 1);
fwrite ( &block_buffer, 1, 1, stdout);
}
/* Deinit the encryption thread,
* and unload the module
*/
mcrypt_generic_end(td);
return 0;
}


Listing 10: C my_decrypt command line application
#include <mcrypt.h>
#include <stdio.h>
#include <stdlib.h>
/* #include <mhash.h> */
main( int argc, char *argv[] ) {
char password[20];
if (argc < 2) {
return 1;
} else {
strcpy(password, argv[1]);
return my_decrypt(password);
}
}


int my_decrypt(char *the_pass) {
MCRYPT td;
int i;
char *key;
char password[20];
char block_buffer;
char *IV;
int keysize=16; /* 128 bits */
key=calloc(1, keysize);
strcpy(password, the_pass);
/* Generate the key using the password */
/*
mhash_keygen( KEYGEN_MCRYPT, MHASH_MD5,
key, keysize, NULL, 0, password,
strlen(password));
*/


memmove( key, password, strlen(password));
td = mcrypt_module_open(“rijndael-128”,
“”, “cfb”, “”);
if (td==MCRYPT_FAILED) {
return 1;
}
IV = malloc(mcrypt_enc_get_iv_size(td));
/* Put random data in IV.
* Note these are not real random data,
* consider using /dev/random or
* /dev/urandom.
*/
/* srand(time(0)); */
for (i=0; i< mcrypt_enc_get_iv_size( td);
i++) {
IV[i]=rand();
}
i=mcrypt_generic_init( td, key, keysize,
IV);
if (i<0) {
mcrypt_perror(i);
return 1;
}
/* Encryption in CFB is performed in bytes
*/
while ( fread (&block_buffer, 1, 1, stdin)
== 1 ) {
/* mcrypt_generic (td, &block_buffer,
1); */
/* Comment below and uncomment above to
* encrypt
*/
mdecrypt_generic (td, &block_buffer, 1);
fwrite ( &block_buffer, 1, 1, stdout);
}
/* Deinit the encryption thread, and unload
* the module */
mcrypt_generic_end(td);
return 0;
}


在PHP中可以做如下测试:


<?php
function my_crypt($string) {
return `echo “$string” | my_crypt`;
}


function my_decrypt($string,$password) {
return trim(`echo “$string” | my_decrypt
“$password”`);
}
?>


但这种方法的一个最大的弱点是因为它通过命令行调用,所以任何在系统中的用户可以输入:


# ps -aux


如果此时my_decrypt正在运行,他们就可以看到password。


所有这些说明了你可以不断的为那些进入到你的系统并想要解密你的数据的人制造障碍。但是不论如何被破解的可能性总是存在的。二战的时候英国捕获了一种叫Enigma的加密算法,不管该算法在当时来看是多么的复杂,最终它还是被解密了,并成功的获取到德国的机密信息。同样的,在计算机被入侵的情况下,加密的作用在于为了在对方解密之前,你能够重新取得计算机的控制权争取时间。



结论
现在相信你已经对mcrypt的作用有了一个不错的了解,但还有大量的关于加密算法的相关内容需要去发现。这也是我写此文的目的:帮助你获取一些关于mcrypt的信息,和一些加密算法信息,但并没有打算使它太过全面。


我们还审视了一些问题,比如如何存放密钥,因为PHP是一种明文的脚本更加加剧了这一问题,即使通过PHP编码或者编译成二进制编码也不能提供完全安全的解决方案。
我们通过两个例子:一个属于客户端的数据传输(通过cookie),另一个保存数据在服务器端(SQL)。mcrypt结合PHP能够帮助你在互联网上传输经过密钥加密的安全数据,从加密单调的数据结构到刚完成的情书。


下载原代码


Robert Peake has been a professional PHP developer and project manager since 2000. He is currently CTO for The David Allen Company. Reach him online: cyberscribe at php dot net.

This article was originally published in
Issue 03.04 of the International PHP Magazine.

Links and Literature
Please note that due to recent security concerns about colissions in MD5, SHA1 is now the preferred secure hash algorithm
For more on rot13 and Usenet see http://en.wikipedia.org/wiki/Rot13
Thanks to Eddie Urenda of UCLA for verifying the Debian libmcrypt install procedure
Much more on mcrypt is available at http://mcrypt.sourceforge.net/
For the gory details of MIME compliancy, see RFC 1521 at http://www.faqs.org/rfcs/rfc1521.html
Thanks to Derick Rethans of PHP for pointing out the significance of IV in non-ecb modes
For more on the role of Enigma in World War II see http://www.bbc.co.uk/history/war/wwtwo/enigma_04.shtml