1.什么是RSA加密
RSA (详见维基百科)算法是现今使用最广泛的公钥密码算法,也是号称地球上最安全的加密算法,与 md5 和 sha1 不同,到目前为止,也只有极短的RSA加密被破解。
那么什么是公匙密码算法呢,根据密钥的使用方法,可以将密码分为对称密码和公钥密码,接下来我们来简单说明下它们两个。
- 对称密码:加密和解密使用同一种密钥的方式,常用的算法有 DES 以及 AES。
- 公钥密码:加密和解密使用不同的密码的方式,因此公钥密码通常也称为非对称密码,常用的算法有 RSA。
由于本文讨论的是 php 的 RSA 加密实例,这里就不详细说明了,对于 RSA 算法有兴趣的朋友可以查看下面的文章
《带你彻底理解RSA算法原理》
对于 php 更多加密方式有兴趣的朋友可以查看下面的文章
《PHP数据加密技术与密钥安全管理》
2.使用场景
- 为移动端(IOS,安卓)编写 API 接口
- 进行支付、真实信息验证等安全性需求较高的通信
- 与其他第三方或合作伙伴进行重要的数据传输
3.生成私钥、公钥
既然 RSA 是非对称加密,那么就先必须生成所需要的私钥和公钥,以 ubuntu 为例。首先下载开源 RSA 密钥生成工具 openssl (通常为 linux 系统自带),如果没有,可以通过以下命令进行安装:
apt-get install openssl
当 openssl 安装完毕后,我们就可以开始生成私钥、公钥了。首先,生成原始 RSA 私钥文件:
openssl genrsa -out rsa_private_key.pem 1024
注:这里生成了一个长度为 1024bit 的密钥,转化为字节就是 128byte
其次,将原始 RSA 私钥转换为 pkcs8 格式:
openssl pkcs8 -topk8 -inform PEM -in rsa_private_key.pem -outform PEM -nocrypt -out private_key.pem
最后,生成 RSA 公钥:
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
在需要使用的时候,我们将私钥 private_key.pem 放在服务器端,公钥发放给需要与我们进行加密通信的一方就可以了。
4.php-rsa 加密解密实例
现在我们可以使用 php 进行 RSA 的加密解密了,但在此之前,你首先要看看你有没有安装或开启 php 的 openssl 扩展,可以通过文件输出 phpinfo() 或者通过命令行输出 php -m | less 来查看是否安装开启了此扩展,也可以通过 extension_loaded() 函数来判断扩展是否开启,如果没有,则通过该命令进行安装(以 ubuntu 为例):
apt-get install php7.0-openssl
当然,如果你是 Windows 操作系统,可以下载对应版本的 php_openssl.dll。
好了,现在我们来编写一个 php-RSA 的服务器类库,这个类库的工作其实很简单,就是封装一些 php 针对 RSA 操作的函数,方便我们加密解密。注:这个只是最简单的RSA类,实际用处不大
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| class Rsa { private $_config = [ 'public_key' => '', 'private_key' => '', ];
public function __construct($private_key_filepath, $public_key_filepath) { $this->_config['private_key'] = $this->_getContents($private_key_filepath); $this->_config['public_key'] = $this->_getContents($public_key_filepath); }
private function _getContents($file_path) { file_exists($file_path) or die ('密钥或公钥的文件路径错误'); return file_get_contents($file_path); }
private function _getPrivateKey() { $priv_key = $this->_config['private_key']; return openssl_pkey_get_private($priv_key); }
private function _getPublicKey() { $public_key = $this->_config['public_key']; return openssl_pkey_get_public($public_key); }
public function privEncrypt($data = '') { if (!is_string($data)) { return null; } return openssl_private_encrypt($data, $encrypted, $this->_getPrivateKey()) ? base64_encode($encrypted) : null; }
public function publicEncrypt($data = '') { if (!is_string($data)) { return null; } return openssl_public_encrypt($data, $encrypted, $this->_getPublicKey()) ? base64_encode($encrypted) : null; }
public function privDecrypt($encrypted = '') { if (!is_string($encrypted)) { return null; } return (openssl_private_decrypt(base64_decode($encrypted), $decrypted, $this->_getPrivateKey())) ? $decrypted : null; }
public function publicDecrypt($encrypted = '') { if (!is_string($encrypted)) { return null; } return (openssl_public_decrypt(base64_decode($encrypted), $decrypted, $this->_getPublicKey())) ? $decrypted : null; } }
|
好了,现在我们调用 Rsa 类,对数据进行加密解密
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| $private_key = 'private_key.pem'; $public_key = 'rsa_public_key.pem'; $rsa = new Rsa($private_key, $public_key);
$origin_data = '这是一条测试数据';
$ecryption_data = $rsa->privEncrypt($origin_data);
$decryption_data = $rsa->publicDecrypt($ecryption_data);
echo '私钥加密后的数据为:' . $ecryption_data; echo PHP_EOL; echo '公钥解密后的数据为: ' . $decryption_data; echo PHP_EOL;
|
最后要说明的是,公钥、私钥都可以加密,也都可以解密。其中:用公钥加密需要私钥解密,称为“加密”。由于私钥是不公开的,确保了内容的保密,没有私钥无法获得内容;用私钥加密需要公钥解密,称为“签名”。由于公钥是公开的,任何人都可以解密内容,但只能用发布者的公钥解密,验证了内容是该发布者发出的。
注:做支付的时候,一般来说我这边使用银行的工具自己生成一套公钥私钥对,银行机构也有一套公钥私钥对。我要把自己的公钥上传到银行,然后从网站上复制银行机构的公钥到本地。提交订单到银行的时候,有两个必传参数,encryptData和sign。encryptData是把业务参数排序后,我用银行公钥加密数据赋值给encryptData,银行收到后会用银行私钥进行解密。sign是验证数据是否合法的令牌,使用openssl_sign()和我的私钥生成,由银行进行验证。用户付款后,银行会发送一个异步通知给我,我要使用银行公钥和openssl_verify()来验证通知是不是伪造的。要注意的一点是“签名”和“加密”是两码事。
5.密钥文件后缀是pfx和cer
cer文件相当于公钥(可以发给客户端),pfx是密钥(必须严格保存于服务器端不能泄露)。
php服务端利用内置函数生成公钥和密钥,先生成cer文件和pfx文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| $dn = array( "countryName" => 'zh', "stateOrProvinceName" => 'GuangDong', "localityName" => 'ShenZhen', "organizationName" => 'baibai', "organizationalUnitName" => 'company', "commonName" => 'bbb', "emailAddress" => '123@.qq.com' ); $privkeypass = 'cf'; $numberofdays = 3650; $cerpath = "./test.cer"; $pfxpath = "./test.pfx"; $privkey = openssl_pkey_new(); $csr = openssl_csr_new($dn, $privkey); $sscert = openssl_csr_sign($csr, null, $privkey, $numberofdays); openssl_x509_export_to_file($sscert, $cerpath);
openssl_pkey_export_to_file($privkey, $pfxpath);
|
以下是是php端进行简单测试的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
$cer_key = file_get_contents($pfxpath); openssl_pkcs12_read($cer_key, $certs, $privkeypass); openssl_sign($data, $signMsg, $certs['pkey'],OPENSSL_ALGO_SHA1); $signMsg = base64_encode($signMsg);
$cer_key = file_get_contents($cerpath); $unsignMsg=base64_decode($signMsg); $cer = openssl_x509_read($cer_key); $res = openssl_verify($data, $unsignMsg, $cer); echo $res;
$data = "123456"; $crypted = ""; $key = file_get_contents($cerpath);
openssl_public_encrypt($data, $crypted, $key); echo base64_encode($crypted)."<br>";
$decrypted = ""; $s = file_get_contents($pfxpath);
$key2 = openssl_pkey_get_private(file_get_contents($pfxpath));
if(openssl_private_decrypt($crypted, $decrypted, $key2)){ echo $decrypted; }else{ echo "failed"; }
|
6.其它问题
银行给的公钥是一串字符串
银行给的公钥类似于MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCSEPnYEgIBCgCe1n9n7M7WEMJgwcGaFcJgQ3Jv0s3cQIfH9zqtw+bYISJo93ygE6y3aDGSxE51Z6CFSvk9NeCPICAlaE5hzbMm96aeK+N1tjQHGi/mEweFkVbgn8nvUFTrQakVSvXti7cI79IXmuoqgQ+PsNxm7vM1j3I0UbfeQQIDAQAB这样,要怎么使用?
PHP里公钥需要满足以下条件:
- 公钥中必须有
-----BEGIN PUBLIC KEY----- 和 -----END PUBLIC KEY-----。
- 公钥必须换行,一般每行64个长度。
转换方法:
1 2 3 4 5
| $res = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($str, 64, "\n", true) . "\n-----END PUBLIC KEY-----";
$pubKey = openssl_pkey_get_public($res);
|
创建字符串格式的密钥对
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| public function createKeys($key_size = 2048) { $config = array( "private_key_bits" => $key_size, "private_key_type" => OPENSSL_KEYTYPE_RSA, ); $res = openssl_pkey_new($config); openssl_pkey_export($res, $private_key); $public_key_detail = openssl_pkey_get_details($res); $public_key = $public_key_detail["key"];
return [ "public_key" => $public_key, "private_key" => $private_key, ]; }
生成密钥对格式如下:
|
加密数据长度要求
openssl_public_encrypt()加密失败,返回False。
以私钥加密为例,加密强度1024的私钥,每次可加密数据长度为 1024位/8=128字节,当使用OPENSSL_PKCS1_PADDING(这个要占用11个字节)填充时,可加密1024/8-11=117字节。如果超出,那么这些openssl加解密函数会返回false。所以当超出时,请分段加密!同理:对于2048长度的密钥,如果要加密的数据长度大于245的话,加密失败,只能进行分段加密。
分段加密方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public function encryptBigData($data) { $pubKey = 'XXXXXXXXXXXXXXX....'; $crypto = ''; $res = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($pubKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----"; $openssl_pub = openssl_pkey_get_public($res); foreach (str_split($data, 117) as $chunk) { openssl_public_encrypt($chunk, $encryptData, $openssl_pub, OPENSSL_PKCS1_PADDING); $crypto .= $encryptData; } return base64_encode($crypto); }
|
注:这里使用117字节分段,当然对方也可能是64字节进行分段加密,需要和对方核实并保持一致。
注意加解密前进行编码和解码
一般来说加密后的数据是二进制数据,为了便于传输和可见性,通常会将加密后的数据进行base64编码。同样地,解密前需将密文进行base64解码。
注:需和对方核实,此处有可能使用2进制、16进制编码、解码,如bin2hex、hex2bin
7.实用的RSA类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
| <?php class OpensslRSA { public $private_key = '-----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQC+L7ENzBHxKOqjuFHPFKlAt40BatVZhUAHw/G05XshpTGqm9Rv 8wG0EAbFbdo9PuB8DiXdPQLyIfaqkTgpsPjJ1Ow7WKxmYbqZN5IW/GN+TyFWP+MB 2W6IBLPWBDvAl2NJlmU8j3LCPJW1dH4zP1OitkxZMyUuUYYfPuOtO9RJ2wIDAQAB AoGAUFCbmJQBT7JTxGfmRGkZQLdC2MJg7rkS3TSmMhpm8UJtwvqjr9MTeRL7iQxn CU4wRrNC0jcds1sca9N/wDt4FCkCala+bg7mwQuPpg5QhXelfFr88ibRnP8y8LmZ 7PPNqx9c4jivhMzJrzNh3luqg6awjsig2w3+EW1/Ubb30AECQQDshTvyc5mpDgiO 4g8q1ztszszL9eCp+IjlUaN51vC3Nj1eXpjbtdSZ0JVKrDdhKcd3rEZVYzMQN/lI pyq85e/bAkEAzdmN6TF3Y1h3LouumCy6+61ChTFrl/yjw13CGApmAQHhEVyANHr7 NjoxP06eimzn7KHff/eYxd1Emf1SYA8uAQJBAN1ibFUpLRgXAZ20LNw9r+rNutXi ZJLUBlcXTjv6G0ByLYkKZGuqy7/ZhBPsFL4GnCUBBKhh/ObebaA6kH9VfmcCQGfg 0WxMOiM4EWy7sG+6ouE+ncL5HYKlSz7boYbgOHlpqVpJg6j4Jq1G0HNSCU9xhdg0 F8VL/RxcfLH41AkFoAECQQCR8NDB3BgHqyJfarKKMWQ3qrXHaLfBKExMrpQ8MDzs MlSBzFOnucufo110lSgjdRlgr8smtU2hx9gXFIqxvfWF -----END RSA PRIVATE KEY-----'; public $public_key = '-----BEGIN PUBLIC KEY----- MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+L7ENzBHxKOqjuFHPFKlAt40B atVZhUAHw/G05XshpTGqm9Rv8wG0EAbFbdo9PuB8DiXdPQLyIfaqkTgpsPjJ1Ow7 WKxmYbqZN5IW/GN+TyFWP+MB2W6IBLPWBDvAl2NJlmU8j3LCPJW1dH4zP1OitkxZ MyUuUYYfPuOtO9RJ2wIDAQAB -----END PUBLIC KEY-----'; public $pi_key; public $pu_key;
public function __construct() { $this->pi_key = openssl_pkey_get_private($this->private_key); $this->pu_key = openssl_pkey_get_public($this->public_key); }
public function PrivateEncrypt($data) { $crypto = ''; foreach (str_split($data, 117) as $chunk) { openssl_private_encrypt($chunk, $encryptData, $this->pi_key); $crypto .= $encryptData; } $encrypted = $this->urlsafe_b64encode($crypto); return $encrypted; }
function urlsafe_b64encode($string) { $data = base64_encode($string); $data = str_replace(array('+', '/', '='), array('-', '_', ''), $data); return $data; }
function urlsafe_b64decode($string) { $data = str_replace(array('-', '_'), array('+', '/'), $string); $mod4 = strlen($data) % 4; if ($mod4) { $data .= substr('====', $mod4); } return base64_decode($data); }
public function PublicDecrypt($encrypted) { $crypto = ''; foreach (str_split($this->urlsafe_b64decode($encrypted), 128) as $chunk) { openssl_public_decrypt($chunk, $decryptData, $this->pu_key); $crypto .= $decryptData; } return $crypto; }
public function PublicEncrypt($data) { $crypto = ''; foreach (str_split($data, 117) as $chunk) { openssl_public_encrypt($chunk, $encryptData, $this->pu_key); $crypto .= $encryptData; } $encrypted = $this->urlsafe_b64encode($crypto); return $encrypted; }
public function PrivateDecrypt($encrypted) { $crypto = ''; foreach (str_split($this->urlsafe_b64decode($encrypted), 128) as $chunk) { openssl_private_decrypt($chunk, $decryptData, $this->pi_key); $crypto .= $decryptData; } return $crypto; } }
|
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| include 'OpensslRSA.php'; $rsa = new OpensslRSA(); $rst = array( 'ret' => 200, 'code' => 1, 'data' => [ 'info' => '11111111111111111111111111111111111111111111111111111111111111regporkje5kmjkm]=67o,l6u-l,7,l78-.l79l.9l7.=8l' ], 'msg' => "success", ); $ex = json_encode($rst);
$ret_e = $rsa->PublicEncrypt($ex);
$ret_d = $rsa->PrivateDecrypt($ret_e); echo $ret_e; echo '<pre>'; echo $ret_d;
echo '<pre>';
|