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


<<:  Material UI in React [ Day13 ] Inputs (slider) 调整滑块

>>:  Day 2 - 原型: Figma

大共享时代系列_012_线上视讯会议

视讯会议时,你都偷偷在做什麽? 蓬勃发展的视讯会议软件 拜 covid-19 所赐,因爲许多人的工作...

Day14 Number Guessing

今天要利用之前学到的东西写一个猜数字游戏,此猜数字游戏要符合以下条件: 1 猜数字范围介於0-99间...

Day-14 传值与传址

综合本系列此前的汇整,构成JavaScript资料的基本型别(primitives)是指字串、数字、...

30天Python自学:Day01

#以下内容皆由初学者撰写,有错误可能,不建议尽信 30天Python自学: Day01 Python...

Day 27 阿里云上运行Kubernetes 3 - ACK

在前两篇简单操作了阿里上面的ACK服务,今天要分享的是使用ingress,怎麽用一个IP去分享多个服...