iT邦幫忙

2021 iThome 鐵人賽

DAY 4
0
永豐金融APIs

永豐 API 隨意玩系列 第 4

Day04 - 隨意玩之 AES-CBC 加/解密

加密前的資料在前幾天我們都有拿到了!接著就是實作 AES-CBC 囉~


流程如下圖
https://ithelp.ithome.com.tw/upload/images/20210913/20141787CMXN6RRCdo.png

關於 AES-CBC,可以參考 Wiki
AES-CBC
不過我還是稍微講一下,從圖片可以看出要進行 AES-CBC,需要有幾樣東西

  • Plaintext
  • IV
  • Key

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 了~


上一篇
Day03 - 隨意玩之 API 訊息內文以及 Sign
下一篇
Day05 - 隨意玩之 OrderCreate API
系列文
永豐 API 隨意玩6

尚未有邦友留言

立即登入留言