一、應用背景:
在php服務端與客戶端交互或開放api時,通常需要對敏感的部分api數據傳輸進行數據加密。
二、公鑰私鑰加解密的作用:
(1)、公鑰加密
假設一下,我找了兩個數字,一個是1,一個是2。我把2保留起來,不告訴你們,當做我的私鑰,然後我告訴大家,1是我的公鑰。
我有一個文件,不能讓別人看,我就用1加密了。別人找到了這個文件,但是他不知道2就是解密的私鑰啊,所以他解不開,只有我可以用數字2,就是我的私鑰,來解密。這樣我就可以保護數據了。
我的好朋友x用我的公鑰1加密了字符a,加密後成了b,放在網上。別人偷到了這個文件,但是別人解不開,因爲別人不知道2就是我的私鑰,只有我才能解密,解密後就得到a。這樣,我們就可以傳送加密的數據了。
(2)、私鑰簽名
如果我用私鑰加密一段數據(當然只有我可以用私鑰加密,因爲只有我知道2是我的私鑰),結果所有的人都看到我的內容了,因爲他們都知道我的公鑰是1,那麼這種加密有什麼用處呢?
但是我的好朋友x說有人冒充我給他發信。怎麼辦呢?我把我要發的信,內容是c,用我的私鑰2,加密,加密後的內容是d,發給x,再告訴他解密看是不是c。他用我的公鑰1解密,發現果然是c。
這個時候,他會想到,能夠用我的公鑰解密的數據,必然是用我的私鑰加的密。只有我知道我得私鑰,因此他就可以確認確實是我發的東西。這樣我們就能確認發送方身份了。這個過程叫做數字簽名。當然具體的過程要稍微複雜一些。用私鑰來加密數據,用途就是數字簽名。
三、總結
- 公鑰和私鑰是成對的,它們互相解密。
- 公鑰加密,私鑰解密。
- 私鑰加密,公鑰解密(簽名的一種方式)。
- 私鑰簽名,公鑰驗證。
四、簡單測試:
<?php
$config = array(
"digest_alg" => "sha512",
"private_key_bits" => 4096, //字節數 512 1024 2048 4096 等 ,不能加引號,此處長度與加密的字符串長度有關係,可以自己測試一下
"private_key_type" => OPENSSL_KEYTYPE_RSA, //加密類型
);
$res = openssl_pkey_new($config);
//提取私鑰
openssl_pkey_export($res, $private_key);
//生成公鑰
$public_key = openssl_pkey_get_details($res);
// var_dump($public_key);
$public_key=$public_key["key"];
//顯示數據
var_dump($private_key); //私鑰
var_dump($public_key); //公鑰
//要加密的數據
$data = "http://www.cnblogs.com/wt645631686/";
echo '加密的數據:'.$data."\r\n";
//私鑰加密後的數據
openssl_private_encrypt($data,$encrypted,$private_key);
//加密後的內容通常含有特殊字符,需要base64編碼轉換下
$encrypted = base64_encode($encrypted);
echo "私鑰加密後的數據:".$encrypted."\r\n";
//公鑰解密
openssl_public_decrypt(base64_decode($encrypted), $decrypted, $public_key);
echo "公鑰解密後的數據:".$decrypted,"\r\n";
//----相反操作。公鑰加密
openssl_public_encrypt($data, $encrypted, $public_key);
$encrypted = base64_encode($encrypted);
echo "公鑰加密後的數據:".$encrypted."\r\n";
openssl_private_decrypt(base64_decode($encrypted), $decrypted, $private_key);//私鑰解密
echo "私鑰解密後的數據:".$decrypted."n";
五、封裝:
<?php
/**
* RSA算法類【支持:公鑰加密-私鑰解密;私鑰加密-公鑰解密;私鑰簽名-公鑰驗籤】
* 簽名及密文編碼:base64字符串/十六進制字符串/二進制字符串流
* 填充方式: PKCS1Padding(加解密)/NOPadding(解密)
*
* Notice:Only accepts a single block. Block size is equal to the RSA key size!
* 如密鑰長度爲1024 bit,則加密時數據需小於128字節,加上PKCS1Padding本身的11字節信息,所以明文需小於117字節
*
* 用openssl生成rsa密鑰對(私鑰/公鑰)命令:
* openssl genrsa -out rsa_private_key.pem 1024
* openssl rsa -pubout -in rsa_private_key.pem -out rsa_public_key.pem
*/
class Rsa
{
private $pubKey = null;
private $priKey = null;
private $isFile = null;
/**
* 構造函數
* @param string 公鑰文件(驗籤和加密時傳入)
* @param string 私鑰文件(簽名和解密時傳入)
* @param string
*/
public function __construct($public_key_file = '', $private_key_file = '', $isFile = true)
{
// 證書是否是文件
$this->isFile = $isFile;
// 若未指定公鑰,則使用默認公鑰
$public_key_file = $public_key_file ? $public_key_file : __DIR__ . DS . 'cert' . DS . 'public.pem';
$this->_getPublicKey($public_key_file); // 從文件中提取公鑰
// 若未指定私鑰,則使用默認私鑰
$private_key_file = $private_key_file ? $private_key_file : __DIR__ . DS . 'cert' . DS . 'private.pem';
$this->_getPrivateKey($private_key_file); // 從文件中提取私鑰
}
/**
* 【私鑰簽名】
* @param string 簽名材料
* @param string 簽名編碼(base64/hex/bin)
* @return 簽名值
*/
public function sign($data, $code = 'base64')
{
$ret = false;
if (openssl_sign($data, $ret, $this->priKey)) {
$ret = $this->_encode($ret, $code);
}
return $ret;
}
/**
* 【公鑰驗籤】
* @param string 簽名材料(被簽名數據)
* @param string 簽名值(已經簽名的字符串)
* @param string 簽名編碼(base64/hex/bin)
* @return bool
*/
public function verify($data, $sign, $code = 'base64')
{
$ret = false;
$sign = $this->_decode($sign, $code);
if ($sign !== false) {
switch (openssl_verify($data, $sign, $this->pubKey)) {
case 1:
$ret = true;
break;
case 0:
case -1:
default:
$ret = false;
}
}
return $ret;
}
/**
* 【私鑰加密】
* @param string 明文
* @param string 密文編碼(base64/hex/bin)
* @param int 填充方式(貌似php有bug,所以目前僅支持OPENSSL_PKCS1_PADDING)
* @return string 密文
*/
public function prienc($data, $code = 'base64', $padding = OPENSSL_PKCS1_PADDING)
{
$ret = false;
if (!$this->_checkPadding($padding, 'en')) $this->_error('padding error');
if (openssl_private_encrypt($data, $result, $this->priKey, $padding)) {
$ret = $this->_encode($result, $code);
}
return $ret;
}
/**
* 【公鑰解密】
* @param string 密文
* @param string 密文編碼(base64/hex/bin)
* @param int 填充方式(OPENSSL_PKCS1_PADDING / OPENSSL_NO_PADDING)
* @param bool 是否翻轉明文(When passing Microsoft CryptoAPI-generated RSA cyphertext, revert the bytes in the block)
* @return string 明文
*/
public function pubdec($data, $code = 'base64', $padding = OPENSSL_PKCS1_PADDING, $rev = false)
{
$ret = false;
$data = $this->_decode($data, $code);
if (!$this->_checkPadding($padding, 'de')) $this->_error('padding error');
if ($data !== false) {
if (openssl_public_decrypt($data, $result, $this->pubKey, $padding)) {
$ret = $rev ? rtrim(strrev($result), "\0") : '' . $result;
}
}
return $ret;
}
/**
* 【公鑰加密】
* @param string 明文
* @param string 密文編碼(base64/hex/bin)
* @param int 填充方式(貌似php有bug,所以目前僅支持OPENSSL_PKCS1_PADDING)
* @return string 密文
*/
public function pubenc($data, $code = 'base64', $padding = OPENSSL_PKCS1_PADDING)
{
$ret = false;
if (!$this->_checkPadding($padding, 'en')) $this->_error('padding error');
if (openssl_public_encrypt($data, $result, $this->pubKey, $padding)) {
$ret = $this->_encode($result, $code);
}
return $ret;
}
/**
* 【私鑰解密】
* @param string 密文
* @param string 密文編碼(base64/hex/bin)
* @param int 填充方式(OPENSSL_PKCS1_PADDING / OPENSSL_NO_PADDING)
* @param bool 是否翻轉明文(When passing Microsoft CryptoAPI-generated RSA cyphertext, revert the bytes in the block)
* @return string 明文
*/
public function pridec($data, $code = 'base64', $padding = OPENSSL_PKCS1_PADDING, $rev = false)
{
$ret = false;
$data = $this->_decode($data, $code);
if (!$this->_checkPadding($padding, 'de')) $this->_error('padding error');
if ($data !== false) {
if (openssl_private_decrypt($data, $result, $this->priKey, $padding)) {
$ret = $rev ? rtrim(strrev($result), "\0") : '' . $result;
}
}
return $ret;
}
/**
* 【公鑰加密長數據】
*/
public function longpubenc($data){
$crypto = '';
foreach (str_split($data, 117) as $chunk) {
openssl_public_encrypt($chunk, $result, $this->pubKey);
$crypto .= $result;
}
return base64_encode($crypto);
}
/**
* 【私鑰解密長數據】
*/
public function longpridec($data){
$crypto = '';
foreach (str_split(base64_decode($data), 128) as $chunk) {
openssl_private_decrypt($chunk, $result, $this->priKey);
$crypto .= $result;
}
return $crypto;
}
/**
* 創建證書
* @return boolean
*/
public static function makeCert(){
// 根據字節數、加密類型創建祕鑰及公鑰
$config = ["digest_alg"=>"sha512", "private_key_bits"=>1024, "private_key_type"=>OPENSSL_KEYTYPE_RSA];
$result = openssl_pkey_new($config);
if (! $result){
$this->_error(openssl_error_string());
}
// 提取私鑰及公鑰
openssl_pkey_export($result, $private_key);
$public_key = openssl_pkey_get_details($result);
$public_key = $public_key["key"];
// 校驗目錄是否存在且可寫
$path = __DIR__ . DS ."cert" . DS;
$check_path = is_dir($path) ? is_writable($path) : mkdir($path, 0755, true);
if (! $check_path){
$this->_error("文件不可寫入或目錄無法創建:{$check_path}!");
}
// 導出私鑰及公鑰到文件中
file_put_contents("{$path}cert_public.key", $public_key);
file_put_contents("{$path}cert_private.pem", $private_key);
openssl_free_key($result);
}
/**
* 釋放資源
*/
public function __destruct()
{
is_resource($this->priKey) && @openssl_free_key($this->priKey);
is_resource($this->pubKey) && @openssl_free_key($this->pubKey);
}
/**
* 自定義錯誤處理
*/
private function _error($msg)
{
die('RSA Error:' . $msg); //TODO
}
/**
* 檢測填充類型
* 加密只支持PKCS1_PADDING
* 解密支持PKCS1_PADDING和NO_PADDING
*
* @param int 填充模式
* @param string 加密en/解密de
* @return bool
*/
private function _checkPadding($padding, $type)
{
if ($type == 'en') {
switch ($padding) {
case OPENSSL_PKCS1_PADDING:
$ret = true;
break;
default:
$ret = false;
}
} else {
switch ($padding) {
case OPENSSL_PKCS1_PADDING:
case OPENSSL_NO_PADDING:
$ret = true;
break;
default:
$ret = false;
}
}
return $ret;
}
/**
* 將加密後的數據轉換成base64|hex|bin格式
* @param string $data 加密後的數據
* @param string $code 數據格式
* @return string
*/
private function _encode($data, $code)
{
switch (strtolower($code)) {
case 'base64':
$data = base64_encode('' . $data);
break;
case 'hex':
$data = bin2hex($data);
break;
case 'bin':
default:
}
return $data;
}
/**
* 將需要解密的數據轉換成base64|hex|bin格式
* @param string $data 需要解密的數據
* @param string $code 數據格式
* @return string
*/
private function _decode($data, $code)
{
switch (strtolower($code)) {
case 'base64':
$data = str_replace(' ', '+', $data);
$data = base64_decode($data);
break;
case 'hex':
$data = $this->_hex2bin($data);
break;
case 'bin':
default:
}
return $data;
}
/**
* 提取公鑰
* @param string $file
*/
private function _getPublicKey($file)
{
$key_content = $this->_readFile($file);
if ($key_content) {
self::_formatPubkey($key_content); // 轉換成標準公鑰格式
$this->pubKey = openssl_get_publickey($key_content);
}
}
/**
* 提取私鑰
* @param string $file
*/
private function _getPrivateKey($file)
{
$key_content = $this->_readFile($file);
if ($key_content) {
self::_formatPrikey($key_content); // 轉換成標準祕鑰格式
$this->priKey = openssl_get_privatekey($key_content);
}
}
/**
* 讀取公鑰文件
* @param string $file
* @return unknown|Ambigous <boolean, string>
*/
private function _readFile($file)
{
$ret = false;
if (!$this->isFile) return $file;
if (!file_exists($file)) {
$this->_error("The file {$file} is not exists");
} else {
$ret = file_get_contents($file);
}
return $ret;
}
/**
* 格式化祕鑰(轉換祕鑰格式)
* @param unknown $prikey
*/
private function _formatPrikey(&$prikey)
{
// 如果沒有BEGIN PRIVATE KEY 以及 END PRIVATE KEY 標識,則將此祕鑰進行格式化
if (! (strpos($prikey, 'PRIVATE KEY') !== false)){
$prikey = chunk_split($prikey, 64, "\n");
$prikey = "-----BEGIN RSA PRIVATE KEY-----\n" . $prikey . "-----END RSA PRIVATE KEY-----\n";
}
}
/**
* 格式化公鑰(轉換公鑰格式)
* @param unknown $prikey
*/
private function _formatPubkey(&$pubkey)
{
// 如果沒有BEGIN PRIVATE KEY 以及 END PRIVATE KEY 標識,則將此祕鑰進行格式化
if (! (strpos($pubkey, 'PUBLIC KEY') !== false)){
$pubkey = chunk_split($pubkey, 64, "\n");
$pubkey = "-----BEGIN PUBLIC KEY-----\n" . $pubkey . "-----END PUBLIC KEY-----\n";
}
}
/**
* 將十六進制數據轉換成ASCII字符
* @param string $hex
* @return Ambigous <boolean, string>
*/
private function _hex2bin($hex = false)
{
$ret = $hex !== false && preg_match('/^[0-9a-fA-F]+$/i', $hex) ? pack("H*", $hex) : false;
return $ret;
}
}