PHP

[PHP] 範例實做 RSA, 公鑰、私鑰非對稱加密、解密演算法 - 串接 iOS,Android

(本文於 2017-08-08 補充更新) 很多時候,資料在傳遞間需要也必須做好保密動作,之前分享過 [PHP] 實做 AES 資料加密(含範例) 的方式,而 AES 這樣的加密方式並不 100% 安全! 萬一 Clinet 端 的 “金鑰“ 遭到破解,資料內容就完全曝光、失去意義....
因此更多時候我們會採用 『非對稱加密演算法』 !透過公鑰與私鑰的方式能讓資料加密更安全,更有保障!
具體的做法如下...

 

 

 


 

什麼是 RSA 加密演算法?

Wiki 上定義的 RSA加密演算法是一種非對稱加密演算法 (Wiki 真是偷懶好東西)

簡單來說,加密時需要透過 公鑰 進行,而解密時需要利用 私鑰 進行解密!
也就是說,就算 Client 端遭到破解,造成 公鑰 流出,也不會有任何危害。

‼️ 注意:透過 RSA 加密後的內容(密文),每次都會不同!
因為 演算法 (PKCS PADDING) 的關係,每次加密後的結果都不同,但透過同組公、私鑰都能正常解密.
(PKCS PADDING 會在加密前對明文內容加入亂數填充 Padding - wiki)

 

 


 

如何產生公鑰與私鑰?

在這裡,需要利用 openssl 產出一 私鑰(private key)
你需要在 CLI (command-line interface) 裡使用 opensll 輸入下面指令,windows 的使用者請先安裝 openssl (這次先不另外講解這部份...)

 

可以透過兩種方式建立 公鑰與私鑰,則一即可..

方法一:

>openssl genrsa -out private_1024.key 1024

使用 RSA 1024 位元加密,產出 Private Key

接著再使用剛剛產出的 private key 建立 public key

>openssl rsa -in private.key -out public_1024.pem -outform PEM -pubout

 

 

方法二: (產出的副檔名,不是那麼重要)

>sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout ~/php/private_2048.key -out ~/php/public_2048.crt

上述方式較為嚴僅,可以設定更多細節,可以參考(https://blog.gtwang.org/linux/nginx-create-and-install-ssl-certificate-on-ubuntu-linux/)

一些參數說明....(也可以選擇要不要再加入密碼保護 )

  • req:使用 X.509 Certificate Signing Request(CSR) Management 產生憑證。
  • x509:建立自行簽署的憑證。
  • nodes:不要使用密碼保護,因為這個憑證是 NGINX 伺服器要使用的,如果設定密碼的話,會讓伺服器每次在啟動時書需要輸入密碼
  • days 3650:設定憑證的使用期限,單位是天,如果不想時常重新產生憑證,可以設長一點。
  • newkey rsa:2048:同時產生新的 RSA 2048 位元的金鑰。
  • keyout:設定金鑰儲存的位置。
  • out:設定憑證儲存的位置。

分別產出 私鑰 (Private Key) 和公鑰 (Public Key) ,也確認兩者都放到正確位置就可以開始實做&測試加解密了!

若要將公鑰交給 iOS 或 Android 端,他們會需要 .pem 或 .der 檔,可以參考下面的文章進行轉換
OpenSSL 操作筆記 - 檔案格式轉換

 

 


 

PHP 實做 RSA, 公私鑰非對稱加解密 - 範例

用簡單的 Plain PHP 做範例...



// 加密前原始資料-信用卡資料
$fullText = '{"card_number":"1234567812345678","expiry_date":"202012","cvc2":"000"}';

// 設定公、私鑰檔名
const PRIVATE_KEY = 'private_1024.key';
const PUBLIC_KEY = 'public_1024.pem';

// 如果你是用第二種方法產生
// const PRIVATE_KEY = 'private_2048.key';
// const PUBLIC_KEY = 'public_2048.crt';

// 如果設定密碼
// const PASSPHRASE = 'my_pasword';


function public_encrypt($plain_text)
{
    $fp = fopen(PUBLIC_KEY, "r");
    $pub_key = fread($fp, 8192);
    fclose($fp);
    $pub_key_res = openssl_get_publickey($pub_key);
    if(!$pub_key_res) {
        throw new Exception('Public Key invalid');
    }
    openssl_public_encrypt($plain_text, $crypt_text, $pub_key_res, OPENSSL_PKCS1_OAEP_PADDING);
    openssl_free_key($pub_key_res);
    return base64_encode($crypt_text); // 加密後的內容為 binary 透過 base64_encode() 轉換為 string 方便傳輸
}

function private_decrypt($encrypted_text)
{
    $fp = fopen(PRIVATE_KEY, "r");
    $priv_key = fread($fp, 8192);
    fclose($fp);
    $private_key_res = openssl_get_privatekey($priv_key);
    // $private_key_res = openssl_get_privatekey($priv_key, PASSPHRASE); // 如果使用密碼
    if(!$private_key_res) {
        throw new Exception('Private Key invalid');
    }

    // 先將密文做 base64_decode() 解釋
    openssl_private_decrypt(base64_decode($encrypted_text), $decrypted, $private_key_res, OPENSSL_PKCS1_OAEP_PADDING);
    openssl_free_key($private_key_res);
    return $decrypted;
}

// 將資料進行加密
$r = public_encrypt($fullText);
var_dump($r);

// 將資料進行解密
$r = private_decrypt($r);
var_dump($r);

非常簡單吧~

 

另外... 使用 1024, 2048 更甚至是 3072 加密時所需的加、解密耗時
1024
加解密約 0.0001~0.0006 秒
失敗時約 0.005~0.007 秒

3072
加解密約 0.0003~0.0007 秒
失敗時約 0.006~0.009 秒

做個參考~

 

 


 

PHP openssl 內建函式說明

官方手冊:openssl_public_encrypt
官方手冊:openssl_private_decrypt

上述兩個函式第四個參數 padding 模式可以指定加、解密使用的演算法
(OPENSSL_PKCS1_PADDING, OPENSSL_SSLV23_PADDING, OPENSSL_PKCS1_OAEP_PADDING, OPENSSL_NO_PADDING)
預設為:OPENSSL_PKCS1_PADDING
目前比較推薦 OPENSSL_PKCS1_OAEP_PADDING

 

 


 

iOS, Android 串接 - 注意事項

  1. Server 端產出 private key + public key 後,先自行測試 publice key 加密、private key 解密
  2. 將 public key 提供給 iOS 或 Android 端
  3. 提供必要資訊
    padding mode:OPENSSL_PKCS1_OAEP_PADDING
  4. 建議提供一個 API 做為 server 與 client 兩者之間的加、解密測試用(方便開發!)
    e.g. client 同時傳入自己處理好的 密文明文
    server 解密後與明文做比對,相同 回傳 true,反之 回傳 false
    這樣能加快兩方的開發速度

 

‼️ 注意: 產生金鑰時的長度, RSA 1024 或 RSA 2048 會影響可被加密的明文長度
e.g 1024 bit 的金鑰能加密的明文只有約 128 bytes (實際上再少一些,詳情請看 參考文章 - 密文與明文長度)
因此如果預期明文的長度比較長,建議最初生成 RSA 金鑰的時候設定大一點 2048, 4096...

 

 


 

參考文章

Java 進行 RSA 加解密時不得不考慮到的那些事兒

生成密文的長度和明文長度無關,但明文長度不能超過密鑰長度

3 Comments

    1. 放在任何位置都可以哦,只是你 PHP 能讀取的地方就可以了。
      通常和專案放在一塊,比較方便測試和讀取。
      安全一點的話,可以放到 .ssh 這類,安全性比較高的目錄。

發表迴響