2022年7月26日 星期二

用 Delphi 做 AES 加密, 在 Laravel 裡面解密

從 2011 年開始,Delphi XE2 提供了跨平台的開發能力之後,Delphi 需要跟各種平台溝通,就是日常會發生的事情。雖然 Delphi 從 Delphi 2005 開始,已經把 Indy 作為與 Delphi 或者 RAD Studio 一起預設安裝的網路套件,但對於加密、解密的能力,Indy 仍舊是使用外部函式庫來提供的,而且全世界很多平台對於加解密都是使用 OpenSSL 來製作的。

OpenSSL 是很優秀的函式庫,Indy 也是,但跨平台的時候,要把 OpenSSL 佈建到不同平台上就是一個很辛苦的事情,例如要把 OpenSSL 佈建到 iOS 上面,就不是一件簡單的事情。為了這個不簡單,Delphi 從 XE5 還是 XE6 的版本開始,自己在 FireMonkey 上面開發了一個 NET 套件,當中支援了 HTTPS 的各種加密模式 (SSLv2, SSLv3, TLS 1.0-1.3),通通都從 Delphi 的層級重寫起,這樣在佈建的時候就不用苦惱。但加密演算法就沒有這麼單純了!

我自己的慣用工具,Client 端是 Delphi, Xcode, Android Studio, Server 端這幾年則是 Laravel 7.x-8.x, 主要是以 PHP 為主。概念上是以 Laravel 做 API Server, 跟 Client 端的程式做溝通。雖然 Laravel 透過 HTTPS 也能提供一定的安全性,但我還是想把傳輸過程中一些比較敏感的資料額外加密一下。

而這幾年比較流行、又相對安全一點的加密演算法,就是 AES 了,我自己選擇的是 AES-256-CBC 這個組合,也就是 AES 加密法,金鑰用 256 bits,以 CBC 模式處理多重區塊加密。

因此,就有兩個要面對的課題:1. Client 端要找到能支援這個組合的套件,而且最好是免費的。2. 加密的結果要能夠讓 Laravel 順利解密。

支援 FireMonkey 的加密套件

對於碩士時研究加密演算法與 CA 的我來說,看懂各種加密演算法、參數與使用的方式不會造成困擾,但要我把每一種加密演算法從 OpenSSL 裡面分離出來重製一次,就很煩了。這個網際網路的年代裡,網路上有很多巨人,我們只要能站在巨人的肩膀上,很多事情就可以不用重頭做起。

最近在 GitHub 裡面,我用 AES-256-CBC 跟 Delphi 作為關鍵字,找到了很多的套件,包含 LockBox-2, LockBox-3, Delphi Encryption Compendium 這幾組可以從 Delphi 的 GitIt 套件管理員當中取得的工具。

測試了LockBox-3跟 Delphi Encryption Compendium 這兩組之後,我選擇了 Delphi Encryption Compendium (後文中簡稱 DEC) 來做測試,主要是因為 LockBox-3 的範例不多,而 DEC 支援 FireMonkey 之外,也提供了各種平台的 Console 跟 GUI 範例程式。

在加密工具的使用上,Key, IV, Padding 是三個基本又最重要的元素,第四個元素則是原文跟密文的編碼格式。確認 DEC 本身的加密、解密功能沒問題之後,我就開始著手改寫 DEC 範例程式中的各種功能。

首先是編碼格式,在 DEC 範例程式中,只提供了 HEX 格式的 Key 跟 IV, 也提供開發人員自訂的 Padding Byte (範例程式中稱為 FillerByte),這是一個靈活度超大的工具,但對於只熟悉 Delphi,不熟悉加密解密的開發人員來說,不啻於無字天書。 

但既然我的目標平台是 Laravel,那當然是先追求 DEC 跟 Laravel 之間的互通了。

Laravel 的 encrypt, encryptstring, decrypt, decryptstring 特性

透過 PHPStorm 的 Debug 功能,以 XDebug 進行 break point 的設定之後,我把 Laravel 的 encrypt, encryptstring 深入了解了一下,發現這兩個 function 最後都是呼叫了 OpenSSL 函式庫裡面的 openssl_encrypt 函式,差異只在於是否把加密結果作為物件對待,但我其實只需要對字串加密,所以直接針對 encryptstring 跟 decryptstring 進行了解。

在呼叫了這個 encryptstring 函式之後,會先隨機建立 Initial Vector(後文中簡稱 iv),iv的長度會根據 AES 金鑰的長度而有差異,256 bits 的 AES 加密演算法,其 iv 的長度是 16 bytes,而 OpenSSL 函式庫會隨機產生這 16 bytes 的 iv,並將之回傳,在 Laravel 當中,加密作業完成後,會把 iv, 加密結果這兩個 base64 的編碼字串連結起來,再用 SHA256 d搭配 AES 的 Key 製作 hmac 驗證碼,最終會把這三個值跟一個名為 tag 的空字串做成一個 JSON 格式的物件,再把這個物件做一次 base64 編碼,JSON 物件的格式如下:

{

    "iv":"KSMkCBozRhR3Lxl3MF4OHg==",

    "value":"Cw025jAKF0TLcjxIJ30Vzg==",

    "mac":"e414202d1ae168315bea8d5d68a4e8bc1de22c299f1dd50940e1c8419565d710",

    "tag":""

}

如果沒有做成這樣的結構,且 padding 字元沒有依照特定的規則,openssl_decrypt 在進行解密的時候就會發生解密失敗的問題。

上面提到的這個 mac, 是 Message Authentic Code 的縮寫,在 DEC 套件裡面並沒有提供,但在 FireMonkey 裡面的 System.Hash.THashSHA2 裡面有提供,只是做法有點特別,需要直接呼叫該類別的類別方法 GetHMAC:

    hMacSrcString := base64IV + base64Cipher;
    hMacResult := THashSHA2.GetHMAC(hMacSrcString,
          TFormat_Base64.Decode(base64Key));

特別的 Padding Byte (補位元)

在所有的加密演算法當中,幾乎所有的對稱金鑰加密演算法(Symmetric Encryption Algorithm) 都需要處理補位位元的問題。從早期美國限制出口的 DES, 3DES, 到廣泛使用在各種網路資料交換的 RC4, RC5, 目前最流行的 AES, 全都有這個問題需要處理。

這個問題的由來,是因為這些加密演算法的計算基準都是以單一區塊進行加密,再透過幾種不同的區塊處理模式 (ECB, CBC, OFB, CFB......) 把這些密文區塊做各種不同的處理之後,製作出原始資料的完整密文。而這些區塊的加密,在各種演算法中都有獨特的位元處理,因此需要每個區塊有固定的大小。

Padding的問題,會發生在原始資料的最後一個區塊。以 AES-256-CBC 這個組合來看,它使用 AES 加密演算法,金鑰長度是 256 bits,也就是 32 bytes,以CBC模式來處理每個加密區塊。這個組合的區塊長度是 16 bytes,所以當原始資料的最後一個區塊長度不到 16 bytes 時,就要把原始資料補足到 16 bytes。

前面提過 DEC 開放了所有的參數讓開發人員可以自行設定,這是非常靈活開放的函式庫做法,但要跟 Laravel 所使用的 OpenSSL 當中的 openssl_decrypt 函式能完整互通,就得先知道 openssl_encrypt 對這問題的處理模式才行。

我做了許多測試,發現 openssl_encrypt 所建立的 AES-256-CBC 密文,補位用的資料會隨著最後這個區塊資料的長度改變,所以如果單純的使用同一個位元資料來做補位,怎麼樣都不可能跟 Laravel 的 decryptstring 共通的。我找出了當中的規則,並且已經一一測試過,完全沒有問題了,規則如下表:

原始資料長度需補位長度必須使用的補位位元內容
1 byte 或者 N* 16 + 1 bytes15 byte0xF
2 bytes 或者 N* 16 + 2 bytes14 bytes0xE
3 bytes 或者 N* 16 + 3 bytes13 bytes0xD
4 bytes 或者 N* 16 + 4 bytes12 bytes0xC
5 bytes 或者 N* 16 + 5 bytes11 bytes0xB
6 bytes 或者 N* 16 + 6 bytes10 bytes0xA
7 bytes 或者 N* 16 + 7 bytes9 bytes0x9
8 bytes 或者 N* 16 + 8 bytes8 bytes0x8
9 bytes 或者 N* 16 + 9 bytes7 bytes0x7
10 bytes 或者 N* 16 + 10 bytes6 bytes0x6
11 bytes 或者 N* 16 + 11 bytes5 bytes0x5
12 bytes 或者 N* 16 + 12 bytes4 bytes0x4
13 bytes 或者 N* 16 + 13 bytes3 bytes0x3
14 bytes 或者 N* 16 + 14 bytes2 bytes0x2
15 bytes 或者 N* 16 + 15 bytes1 bytes0x1

如果補位規則沒有依照上面這個表格所列,openssl_decrypt 的解密會回報錯誤。

結語

Delphi 在各種作業系統中需要與其他各式各樣的 Server 端程式溝通已經是日常事務了,本篇的分享主要是希望讓大家在使用 Laravel 作為 Server 端程式,或者是需要使用 openssl 的 openssl_decript 在 Server 端解密時,可以順利的使用 Delphi 在 Client 端程式迅速的完成程式開發,希望大家會喜歡,這個專案我也放在 GitHub 上面了,請放心使用,如果有遇到什麼問題,也歡迎一起討論或者回覆給我。


沒有留言:

張貼留言