加密前的資料在前幾天我們都有拿到了!接著就是實作 AES-CBC 囉~
流程如下圖
關於 AES-CBC,可以參考 Wiki
不過我還是稍微講一下,從圖片可以看出要進行 AES-CBC,需要有幾樣東西
Plaintext 我們已經有了,就是昨天的 JSON 訊息內文
IV 我們也有,就是將 Nonce 經過 SHA256 雜湊後取右邊十六個字元
Key 我們也有,就是前幾天計算出來的 Hash ID
但是需要知道的不只是這樣。
在 AES-CBC 需要將訊息填充 (padding) 到 block size (16 Bytes) 的整數倍
然而填充方式有許多種,需要知道哪一種才能正確將訊息加密~
PHP Sample Code 的 function EncryptAesCBC
的這兩行程式碼就是在提示永豐他們使用哪一種 padding
// 看還差多少 bytes 才到 16 的整數倍
$padding = 16 - (strlen($data) % 16);
// 使用 chr($padding) 當作 padding 重複 $padding 次
$data .= str_repeat(chr($padding), $padding);
這方式就是 PKCS7
!
如果以 block size 為 8 Bytes 的情況下做舉例
# DD 為我們要加密的明文,可是不足 8 的整數倍,缺少 4 bytes,於是填充 4 個 04
... | DD DD DD DD DD DD DD DD | DD DD DD DD 04 04 04 04 |
# DD 為我們要加密的明文,可是不足 8 的整數倍,缺少 5 bytes,於是填充 5 個 05
... | DD DD DD DD DD DD DD DD | DD DD DD 05 05 05 05 05 |
了解上面的東西後,我們就可以按照圖片的流程,生產出 Ciphertext 了!
程式碼部分也不難,當然要先裝一下必須的 module PyCryptodome
pip install pycryptodome
如何使用可以參考 AES CBC 的說明
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
def CBCEncrypt(key, iv, data):
## new 一個 AES CBC cipher
cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
## 將要加密的 data encode 成 utf-8
## 然後使用 pad function 將明文 padding 到 block size
## https://github.com/Legrandin/pycryptodome/blob/master/lib/Crypto/Util/Padding.py#L39
## 從上面網址可以知道他預設使用 pkcs7 這種 padding 方式,我們不需要做任何事情
return (cipher.encrypt(pad(data.encode('utf-8'), AES.block_size)))
enc_msg = CBCEncrypt(hash_id, iv, msg)
然後永豐也很貼心有提供網址讓使用者去測試自己是否有成功加密!
加密成功後,當然就是要解的回來呀!
解密流程跟加密沒有差很多就不多做解釋了
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import codecs
def CBCDecrypt(key, iv, data):
data = codecs.decode(data, "hex") ## 或是要用 bytes.fromhex(data) 也行
cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
return unpad(cipher.decrypt(data), AES.block_size)
dec_msg = CBCDecrypt(hash_id, iv, enc_message)
永豐也有提供解密網頁讓使用者嘗試
沒錯,就這樣大功告成了!!!
一樣在最後附上到目前為止的 code
from __future__ import unicode_literals
import requests
import hashlib
import codecs
import json
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
shop_no = '<shop_no>'
sinopac_hash = {
'a1': '',
'a2': '',
'b1': '',
'b2': ''
}
nonce_url = 'https://apisbx.sinopac.com/funBIZ/QPay.WebAPI/api/Nonce'
order_url = 'https://apisbx.sinopac.com/funBIZ/QPay.WebAPI/api/Order'
def getNonce():
nonce_data = {
'ShopNo': shop_no
}
r = requests.post(nonce_url, json=nonce_data)
return json.loads(r.content)['Nonce']
def calcHashID():
a1 = sinopac_hash['a1']
a2 = sinopac_hash['a2']
b1 = sinopac_hash['b1']
b2 = sinopac_hash['b2']
xor1 = hex(int(a1, base=16)^int(a2, base=16))
xor2 = hex(int(b1, base=16)^int(b2, base=16))
hash_id = xor1[2:]+xor2[2:]
return hash_id.upper()
def calcIV(nonce):
s = hashlib.sha256()
s.update(nonce.encode('utf-8'))
h = s.hexdigest()
return h[-16:].upper()
def calcSign(msg_content, nonce, hash_id):
sign_msg = msg_content+nonce+hash_id
s = hashlib.sha256()
s.update(sign_msg.encode('utf-8'))
h = s.hexdigest()
return h.upper()
def parseQueryData(msg_param):
if type(msg_param) != dict:
return
order_message = dict(sorted(msg_param.items(), key = lambda x: x[0]))
message = ''
for k, v in order_message.items():
if type(v) == dict or v == '':
continue
message += f"{k}={v}&"
return message[:-1]
def CBCEncrypt(key, iv, data):
cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
return (cipher.encrypt(pad(data.encode('utf-8'), AES.block_size)))
def CBCDecrypt(key, iv, data):
data = codecs.decode(data, "hex")
cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
return unpad(cipher.decrypt(data), AES.block_size)
order_create = {
"ShopNo": shop_no,
"OrderNo": "A202109120010",
"Amount": 50000,
"CurrencyID": "TWD",
"PayType": "C",
"CardParam": {
"AutoBilling": "Y"
},
"ConvStoreParam": { },
"PrdtName": "信用卡訂單",
"ReturnURL": "http://10.11.22.113:8803/QPay.ApiClient/Store/Return",
"BackendURL": "http://10.11.22.113:8803/QPay.ApiClient/AutoPush/PushSuccess"
}
nonce = getNonce()
hash_id = calcHashID()
iv = calcIV(nonce)
content = parseQueryData(order_create)
signature = calcSign(content, nonce, hash_id)
msg = json.dumps(order_create, ensure_ascii=False).replace(' ','')
print(msg)
enc_msg = CBCEncrypt(hash_id, iv, msg).hex().upper()
dec_msg = CBCDecrypt(hash_id, iv, enc_msg)
print(dec_msg)
"""
msg:
{"ShopNo":"<shop_no>","OrderNo":"A202109120010","Amount":50000,"CurrencyID":"TWD","PayType":"C","CardParam":{"AutoBilling":"Y"},"ConvStoreParam":{},"PrdtName":"信用卡訂單","ReturnURL":"http://10.11.22.113:8803/QPay.ApiClient/Store/Return","BackendURL":"http://10.11.22.113:8803/QPay.ApiClient/AutoPush/PushSuccess"}
dec_msg:
{"ShopNo":"<shop_no>","OrderNo":"A202109120010","Amount":50000,"CurrencyID":"TWD","PayType":"C","CardParam":{"AutoBilling":"Y"},"ConvStoreParam":{},"PrdtName":"\xe4\xbf\xa1\xe7\x94\xa8\xe5\x8d\xa1\xe8\xa8\x82\xe5\x96\xae","ReturnURL":"http://10.11.22.113:8803/QPay.ApiClient/Store/Return","BackendURL":"http://10.11.22.113:8803/QPay.ApiClient/AutoPush/PushSuccess"}
"""
### "PrdtName":"\xe4\xbf\xa1\xe7\x94\xa8\xe5\x8d\xa1\xe8\xa8\x82\xe5\x96\xae" 是信用卡訂單
所以到目前為止 API 所需要內容我們都有了!
明天我們就正式開始發 API Request 了~