最近有一個業務需求,需要前端傳遞一個密碼到后端,期間要對傳遞的密碼通過進行對稱加密,我們約定使用成熟的AES加密方法。
前端使用php,后端用python,但是發現前端兄弟加密后的字符串,在python端解密后末尾總會有16字節長度的\x10
字符內容,通過python的ord('\x10')
輸出可知,這就是數字16
的Unicode code。
眾所周知,在使用AES進行對稱加密之前,需要將加密的內容長度補全至16的倍數。如果前端兄弟無法解決加密內容中總有額外的16字節\x10
字符的問題,那么后端就要考慮多余的處理邏輯,看起來奇奇怪怪的。
於是我百度了下php的openssl_encrypt
函數,發現其中option選項有4個:
- 0
- OPENSSL_RAW_DATA=1
- OPENSSL_ZERO_PADDING=2
- OPENSSL_NO_PADDING=3
其中赫然寫着OPENSSL_NO_PADDING
,字面意思很好理解了,應該就是就是不會自動追加(補全)的意思,再看前端兄弟用的是OPENSSL_RAW_DATA
。於是替換為OPENSSL_NO_PADDING
后,果然沒有了\x10
的內容,問題暫時解決了。
然后我又回頭想了一想,為什么OPENSSL_RAW_DATA
會自動追加一個16字節的\x10
呢,這肯定是有原因的。
因為在之前的測試中,我們在調用php的openssl_encrypt
函數之前,已經手動對加密的字符進行了補全,保證其長度是16的倍數。如果不補全會怎樣?
我手動試了一下:
<?php
$str = '1234567890'
$add_data_zero_padding = openssl_encrypt($str, 'AES-128-CBC', $key, $options=OPENSSL_ZERO_PADDING, $iv);
$add_data_no_padding = openssl_encrypt($str, 'AES-128-CBC', $key, $options=OPENSSL_NO_PADDING, $iv);
$add_data_raw_data = openssl_encrypt($add_str, 'AES-128-CBC', 'eNg6geeCinee0kee', $options=OPENSSL_RAW_DATA, 'nesejeiP6du0quie');
var_dump($add_data_zero_padding);
var_dump($add_data_no_padding);
var_dump($add_data_raw_data);
echo "base64 encode:\n";
var_dump(base64_encode($add_data_raw_data));
?>
然后輸出結果就是:
bool(false)
bool(false)
string(32) "�q$B�7��*���vE0�+��J.8t�[Bt�"
base64 encode:
string(44) "jHEkQrs3hBG+DiqE/4B2RTCUK6wE5r1KLjh03VtCdPs="
果然,如果沒有補全,那么OPENSSL_ZERO_PADDING
和OPENSSL_NO_PADDING
會加密失敗。而OPENSSL_RAW_DATA
加密的內容,解密后的字節內容是:
b'NulhIKidvmW6jaFK4T9uqJyuwrlEo\x03\x03\x03'
如此一來,其實不用去細看文檔也能推理出OPENSSL_RAW_DATA
自動補全的含義了,因為補全的內容最后還需要還原為原始字符串,怎么知道哪些字符是補全上去的,哪些字符是原始字符呢?
php邏輯是這樣的,我補全的長度至少是1,最長是16,代表這個長度的數字,正好都可以用一個Unicode字符表示,比如1就是\x01
,16就是\x10
。
如果加密的內容長度是15字節,那么就在最后補全一個\x01
,還原的時候,只需要讀取最后一個字節內容,轉換為數字,得到1
,就知道加密前只追加了1
個字節,那么就把末尾的1
個字節內容去掉即可。
如果加密的內容長度正好是16字節呢,為了還原,那么就必須要在末尾追加16
個\x10
,還原的時候讀取最后一個字節並轉換為數字,就知道加密時候追加了16
字節,那么把末尾的16
個字節去掉即可。
其實用python代碼表示這個補全和還原的邏輯如下:
BLOCK_SIZE = 16 # 16 Bytes
pad = lambda s: s + (BLOCK_SIZE - len(s) % BLOCK_SIZE) * chr(BLOCK_SIZE - len(s) % BLOCK_SIZE) # 至少會追加16字節的內容
unpad = lambda s: s[:-ord(s[len(s) - 1:])]
chr
和 ord
含義如下:
chr(i, /)
Return a Unicode string of one character with ordinal i; 0 <= i <= 0x10ffff.
ord(c, /)
Return the Unicode code point for a one-character string.