2022年7月26日 星期二

Encrypt in Delphi, Decrypt in Laravel

Delphi provided cross-platform application ability from Delphi XE2, that happened in 2011 or 2012. With the feature, all the applications created by Delphi needs to communicate with client applications or server applications created by various IDE or technologies. RESTful API is the most frequently channel.

Although Delphi deliver package bundled Indy from Delphi 2005, and Indy did provided wonderful internet protocol capability, Indy did not provide the capability for encryption and decryption. The security protocol of Indy comes with OpenSSL, which is almost best security library in the world from early 1990 era.Even OpenSSL is so great, it's still difficult to deliver OpenSSL to various operating systems. For example, iOS, Linux. 

For solving the issue about deliver SSL/TLS secure channel to iOS/Linux, Embarcadero created NET package to provide secure channel feature in FireMonkey framework since XE5 or XE6. Developers can use NET package for RESTful API communication without any overheads. What we developers need to do is to create application, package it with the request by Apple/Linux platform, no more extra library issues happen.

Howerver, it's not so easy for encryption and decryption. In FireMonkey, all secure channel features depend on the platform native solutions. In macOS, Delphi used mac safari libraries for SSL/TLS. In iOS, Delphi used iOS native framework for SSL/TLS. And Delphi used Windows/Linux native API in Windows/Linux platform. What can we do to provide encrypt/decrypt feature?

I used to work with Delphi, Xcode, Android studio for creating client application, and used to create server side applications with PHP, and Laravel/PHPStorm in recent years. I do know that Laravel framework provide strong capability, and Apache will provide standard SSL/TLS channel, but I still wish to encrypt some sensitive content in the secure channel, and I believe some of you do think so.

And I tried to pick a popular symmetric encryption, AES is my choice. As we know, all encryption support various key length and block process mode. My choice is AES-256-CBC, which means AES with 256 bits length key and CBC mode.

For this choice, I need to solve 2 issues: 

  1. Picking a encryption/decryption library which supports Delphi, and no overhead in delivery package.
  2. The created cipher should be decrypted by Laravel.

Encryption package for Delphi

I studied information security in graduated school, and my master thesis is about CA and personal certificate management in 1998. Encryption is not big deal for me. It's a huge deal for me to create all the encryption algorithms by myself. Thank God, we have so many giants on the internet, and we need to stand on giant's shoulder, rather than create wheel from zero.

For this thought, I searched the key word "AES-256-CBC" from github in recent weeks, and so many relative projects pop-up, including LockBox-2, LockBox-3, and Delphi Encryption Compendium, all 3 projects can be found in Delphi GetIt package manager GUI.

After testing LockBox-3 and Delphi Encryption Compendium. I chose Delphi Encryption Compendium (I will used DEC for it in later article) as my further test candidate because I cannot get enough example or document for LockBox-3, and I got enough sample projects in DEC for FireMonkey with Console/GUI.

As we know, Key, initial vector (iv for short in later article), and padding byte are 3 fundmental factors for symmetric encryption. And 4th factor might be the encoding format for cipher. I checked that basic encryption/decryption feature works, and then I tried to modify the parameter/format/iv generation in the DEC sample project.

First of all, the encoding format. The DEC sample project requires HEX format key and iv, and HEX is the only one format it supports. The sample project allowed us to use any custom padding byte, it's very flexible for developer, and it's good for DEC.

But it's very hard to understand for developers who are not familiar to encryption/decryption.

I had chosen Laravel as server side application, hence I just try to make the communication between DEC and Laravel at this moment.

The profile of encrypt decrypt, encryptstring, decryptstring in Laravel

With XDebug and PHPStorm, I created a test project to trace into encrypt, encryption, decrypt, decryptstring function. Both of the encrypt functions invoke the openssl_encrypt function, and both of the decrypt functions invoke the openssl_decrypt function.

The 2 functions encrypt and encryptstring are the basically identical, encryptstirng will pack the string as a JSON Payload before invoke encrypt, and the reverse sequence happened in decryptstring and decrypt.

When you invoke encrypt/encryptstring, a random iv will be created, and the length of the iv will be different with the encrypt algorithm and key length. For example, AES-256-CBC will generate 16 bytes iv, and the OpenSSL library function will generate the 16 bytes iv randomly, encode it, combine in a JSON payload, and return it.

The result will be the following JSON payload:

{

    "iv":"KSMkCBozRhR3Lxl3MF4OHg==",

    "value":"Cw025jAKF0TLcjxIJ30Vzg==",

    "mac":"e414202d1ae168315bea8d5d68a4e8bc1de22c299f1dd50940e1c8419565d710",

    "tag":""

}

In the above JSON payload, there are 4 elements:

iv: generated by OpenSSL library, and encoded by base64 format.

value: the cipher, encoded by base64 format.

mac: SHA256.hmac(iv + value).base64String // just psudo code, generate hmac by (iv+value), and encoded it with base64.

tag: empty string. 

If the payload does not match above structure, decryption will be failed. Another important factor is padding byte, padding byte MUST following a rule. The "mac" field in above structure is stand for Message Authentic Code, which is generated by SHA256 hmac, the source date is iv contact value, take above data as example, the source string is: 

Cw025jAKF0TLcjxIJ30Vzg==KSMkCBozRhR3Lxl3MF4OHg==

SHA256 hmac requires a key to protect the source, and we will use the encrypt key as the key again. Howerver, DEC does not provide hmac function, hence we need to use the System.Hash.THashSHA2 in FireMonkey. it's a class function of THashSHA2 class, you need to invoke it as following way:

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

Particular Padding Byte in OpenSSL

In all symmetric encryption algorithms, padding is an important issue. Including DES, 3DES, RC4, RC5, AES, padding is an issue which could not be ignored.

That's caused by the encryption algorithms used block calculation with various block process modes (ECB, CBC, OFB, CFB), all data will be divided into blocks as same size, calculated with key to generate cipher blocks, and process these cipher blocks as completed cipher.

The padding issue will happen in last block, which might be shorter or equal to the block size. If it's shorter, padding byte will be padded in the tail, to make sure the last block size matches the defined block size.

Taking AES-256-CBC as example, the key size is 256 bits (32 bytes), and both iv and block size are 16 bytes. If last block length is less than 16 bytes, we need to pad it to 16 bytes with padding byte.

I mentioned that DEC opens all parameters for developers to config, it's flexible. As we need to make DEC production be decrypted by Laravel decryptstring, we need to figure out how openssl_decrypt handle the padding issue.

I tried every combination from DEC and Laravel, and one conclusion was found: the padding byte value will change with the last block length. If we need to pad 1 byte for the last block, the padding byte should be 0x1, please refer the following table:

Plain Data length
Padding size
Padding Byte value
1 byte or N* 16 + 1 bytes15 byte0xF
2 bytes or N* 16 + 2 bytes14 bytes0xE
3 bytes or N* 16 + 3 bytes13 bytes0xD
4 bytes or N* 16 + 4 bytes12 bytes0xC
5 bytes or N* 16 + 5 bytes11 bytes0xB
6 bytes or N* 16 + 6 bytes10 bytes0xA
7 bytes or N* 16 + 7 bytes9 bytes0x9
8 bytes or N* 16 + 8 bytes8 bytes0x8
9 bytes or N* 16 + 9 bytes7 bytes0x7
10 bytes or N* 16 + 10 bytes6 bytes0x6
11 bytes or N* 16 + 11 bytes5 bytes0x5
12 bytes or N* 16 + 12 bytes4 bytes0x4
13 bytes or N* 16 + 13 bytes3 bytes0x3
14 bytes or N* 16 + 14 bytes2 bytes0x2
15 bytes or N* 16 + 15 bytes1 bytes0x1

If you did not follow the rule in above table, the openssl_decrypt will return failure.

Conclusion

It's very frequently to use Delphi client to communicate with other server technologies. I share this article to make sure every Delphi developer not take time to find a FireMonkey way to do so.

Laravel or any other server side technologies adopt OpenSSL very frequently, so if you check this article out, and use the sample with your Delphi project, it will be very useful.

The sample code is uploaded to github, you can use it as free, I shared the project with Apache License 2.0.


用 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 上面了,請放心使用,如果有遇到什麼問題,也歡迎一起討論或者回覆給我。


2022年7月13日 星期三

Delphi 是否還應該保留對Goto/Label, With 的支援?

 Pascal 時代的遺產

熟悉或者使用了 Delphi 一小段時間的開發人員,都知道 Delphi 的語言是 Object Pascal,而 Object Pascal 是從傳統的 Pascal 演進而來的,這個歷史可能得回溯到 1993-1995 之間,Turbo Pascal 6, Turbo Pascal 7, 以及 Delphi 誕生的那個時代,提到那個年代,就不可避免的必須提到 Anders Hejlsberg 這位大神的貢獻,在 Borland,他改進了 Turbo Pascal 6.0 到 Delphi 7 之間的所有記憶體管理功能,之後轉戰微軟創造了 C# 程式語言與 .NET 平台,最近這幾年又創造了 TypeScript,如果過去三十年當中必須選擇一位我最崇拜的技術人員,非 Anders Hejlsberg 莫屬。

嘮叨了一下簡單的歷史,在 Pascal 時代延續到 Delphi 的 Object Pascal 語法,到目前的 Dephi 11 Alexandria,即使 Delphi 已經加入對於 Windows/macOS/iOS/Android/Linux 各種平台的支援,也加入了 Parallel (平行處理)  的功能,讓多核心的系統能真正發揮最大效能,但 Pascal 時代的遺產還是一直存在。

雖然對於傳統 Pascal 的語法支援可以讓大多數熟悉傳統 Pascal 的開發人員更快上手 Delphi,但是這些語法在新支援的功能中還是有些風險的。讓我們一起來看一下當中的問題。

絕對不要使用的語法:GOTO/LABEL

從 1998 年開始,筆者在攻讀博士的時候,也開始在學校兼課,在所有的程式開發課程中,只要開始講到 LOOP, Branch, Function, 都不免會講到這個最古老的語法:GOTO + LABEL,但筆者也一定會跟課堂的所有學生說『這個語法,禁用!在我課堂上的所有作業中,出現GOTO語法,一律 0 分』

GOTO語法在筆者唸高中剛學 BASIC 語言的時候,是不得不使用的功能,畢竟當時的作業系統是單工的DOS,也沒有多核心、多線程、網路或其他週邊可能來干擾,用 GOTO 讓程式碼得到迴圈的功能無可厚非。

但一旦引入了副程式的概念之後,不管是在 BASIC 裡面的 sub, 還是 Pascal 裡面的 procedure/function 或者 C 的 function, 更可怕的是在 OOP 當中的 Method,都有維護、管理變數、屬性生命週期的議題需要注意,即使不用 GOTO 這個惡魔語法,一個不慎都可能讓整個程式 crash,如果有傻傻搞不清楚的開發人員或者開發學員,在 function A 裡面設定了個 LABEL,在另一個 function B 裡面直接 GOTO 到該 LABEL,下場就是程式直接巴比Q了。

現在還有更嚴重的情況,就是在 Class A 的 Method 當中寫了 Label,但從 Class B 的某個 Event Handler 用了 Goto 跳過去,如果這時候 Class A 根本不曾被建立出來,整個程式也是直接 GG..... 或者是在匿名函式當中做了 Label, 從其他 Method 中 Goto, 執行中的程式也是直接 Bye 了.

如果是簡單的工具程式,幫大家做做簡單的計算機或者文字編輯就算了,但如果是排版、圖片處理的應用程式,在輸入了很多資料,還沒存檔之前,突然直接 crash,絕對會讓使用者直接問候開發人員或者提供應用程式的人家裡的祖宗十八代。

所以,修過筆者開課的所有同學都會牢牢的記住『不可以用GOTO,不然一律0分』這個鐵律,也請大家絕對不要用它。只有組合語言是個例外,因為不用 jumper,就沒得玩了!

不建議使用的語法: With

在傳統 Pascal 當中,Record 跟 Packed Record 常被用來作為自訂型別的結構,由於當中可以把多種型別組合成一個新的型別,在還沒有OOP的那個年代,這個做法已經接近現代OOP的屬性概念(當然當時還沒演進到資料封裝的能力、也還不能包含Method的程式碼在 record 裡面,這些功能都在 Delphi 10.4, 11 的這幾個版本當中被加入了)。

為了方便程式碼的撰寫,傳統 Pascal 提供了 With 這個語法,透過 With,開發人員可以不用把完整的變數名稱每次都 key 出來,省下了不少的打字時間。但是有一長就有一短,在With 當中可以一次標明多個變數,如果這幾個變數當中,出現了同名的屬性,在編譯的時候可能會有語焉不詳(ambigution)的情況發生,而在後續的維護中,也可能讓接手的ㄎ發人員弄錯該程式碼的原意,這是很糟糕的情況。

結論

綜合以上的說明,筆者的想法是『GOTO/LABEL』應該要從Delphi 的 Object Pascal 語法中移除,連出現都不該出現。而 With 語法則是建議編譯時出現提示警告,再過一兩個版本就不支援。希望 Embarcadero 能盡早為了 Delphi 的產出程式更穩定而做出這個修改。

您也有想要建議 Embarcadero 移除掉的傳統 Pascal 語法嗎? 歡迎在底下留言,筆者在參加 Embarcadero MVP 會議的時候會幫大家提出的。