Day05 - [丰收款] 继续把加密这件事看下去

中秋连假还要写铁人赛的文章真的是有一点吃力,虽然平常也只有晚上下班後可以撰写文章,但连假毕竟有较多需要和家人一起互动的时间,要专注将心思放在文件撰写与规格研究和测试上面,真的有一定的难度。

在昨天把Hash ID准备好後,接下来就是另一个要素:IV值。

IV值

在准备IV值前,稍为用很简短的篇幅说明一下AES-CBC加解密法的明文、Key与IV的作用与关系。但由於解说AES原理的文章相当多,有兴趣可以自行搜寻与阅读,在此只解说让没用过AES-CBC的朋友了解为什麽我们需要这些东西的关键概念。

首先,AES-CBC是一个对称式的区块加密法,这里的「对称」指的是加密与解密所需要用到的金钥是相同的,也就是上面提到的「Hash ID」或者「AES Key」。
而加密技术,伴随而来的总是解密(被破解)的议题。如何增加被破解的难度与降低风险?简单的说,如果你有一段一模一样的明文,经由相同的金钥去加密,很显然的每一次执行加密,都会产生出一模一样的结果。这样对於有心人士若能够获取足够的样本资料,就有机会从中尝试破解与分析,找出加密过程的可能规则。

於是就有了IV初始向量的概念,简单说IV并非「密码」,在原本加密过程中再加上一个参数(原则上每一次的加密IV都要不一样),如此一来,可让原本相同明文使用相同金钥产生的密文结果具备高度的差异性,因此可大大增加破解与分析的困难性。

还记得前面提到每一次要呼叫API时都要重新问一次且60秒就会过期的Nonce值吗?他就是为了产生每次做AES-CBC区块加密的IV值作为的基础。

接下来就是要进行永丰API中双方约定好的IV产生方式,也就是要把Nonce再进行一些些加工。

https://ithelp.ithome.com.tw/upload/images/20210918/20130354Fn6DB6bnq7.png

开发规格书中提到IV的产生方式

Nonce 值经过 SHA256 运算後取右边 16 位元字串

又出现了一个关键字SHA256,一样简单描述一下,SHA256是安全杂凑演算法2(SHA-2, Secure Hash Algorithm)的一个实作版本,而杂凑的目的是可快速的将任何不固定长度的原文经过演算後导出一个固定长度的摘要(摘要长度会依实作版本不同)。

若有需要进一步了解,这篇文章解释的蛮清楚的:https://codertw.com/%E7%A8%8B%E5%BC%8F%E8%AA%9E%E8%A8%80/602774/

原文AHASH运算摘要B的过程,可以当成一种产生识别性代号的过程,而且是不可逆的(无法从B转回A),但需要强调的是这并非是「加密」之目的。他可以提供的好处是一来可不需拿A明文来做资料交换之使用,只需使用转化过且较短的摘要B来使用,因为B摘要有一定程度可代表的是A所压缩演化来的,即使稍为被修改过1%内容的A',产生出来的摘要会和B有显着的不同。

另一种目的是我在不需要储存纪录A原文的状态下,要知道这个A和当初的A是相同的时候,只需要储存纪录相对应的摘要B,和即时作Hash运算後看B是否相同,可以以此推论来源的A是相同的。例如一般的会员系统中,在资料库中为了避免资料可能外泄或被有权限取得资料的人非法使用,因此不会储存会员的明文密码,仅会储存摘要杂凑码,在会员登入时才动态比对两者的摘要是相同。这也就是许多网站点选「忘记密码」时,只能让你重新改掉密码,而无法「提供当初你的密码」给你的原因。

因此如果有网站的「忘记密码」功能可以把原本你的明文密码寄给你的话,就要小心一下这个网站保护会员机敏资料的方式了。

接着我们就使用Python语言来实作一下,透过相关的杂凑套件即可快速产生我们要的结果,但在此之前可先使用SHA256 Online的网站先行确认一下我们想要做的结果会是什麽。
由於Nonce值是每一次都需要重启,我们目前先是验证运作的可行性,因此可以先拿开发规格书中的Nonce值来进行基础的验证。

(上面放入Nonce的值,下方会是SHA256的结果,但我们只需要最右边16位字串)

https://emn178.github.io/online-tools/sha256.html

https://ithelp.ithome.com.tw/upload/images/20210918/201303540bnS3R2S6D.png

因此我们用Python程序来撰写时,也希望可以拿到右侧尾码的CB6FA68E42B655AB这个结果。

import hashlib
nonce = "NjM2NjA0MzI4ODIyODguMzo3NzI0ZDg4ZmI5Nzc2YzQ1MTNhYzg2MTk3NDBlYTRhNGU0N2IxM2Q2M2JkMTIwOGU5YzZhMGFmNGY5MjA5YzVm"
iv = hashlib.sha256(nonce.encode('UTF-8')).hexdigest().upper()[-16:]

print(iv)
#output: CB6FA68E42B655AB
程序说明

使用Python的SHA先引入hashlib,将nonce以UTF-8编码格式进行SHA256的16进位杂凑演算後,再依规定转成全大写字,并取得右侧末位的16个字元作为IV最终结果。

看起来是成功的,这个输出结果和我们使用SHA256 Online的网站看到的是一样的。

食材蒐集好了,可以进行AES加密了!

我们花了一些时间,终於把内文讯息Hash ID (AES Key)IV三样食材准备好了,接下来就可以运用最後的AES-CBC神奇区块加密锅,把最终需要的成品Message给制作出来。

https://ithelp.ithome.com.tw/upload/images/20210918/20130354hax9GSbcEk.png

把上面这三样再作个盘点整理:

  1. 讯息内文
{
    "ShopNo": "NA0249_001",
    "OrderNo": "C201804300001",
    "Amount": 50000,
    "CurrencyID": "TWD",
    "PayType": "C",
    "CardParam": { "AutoBilling": "Y" },
    "PrdtName": "信用卡订单",
    "ReturnURL": "http://10.11.22.113:8803/QPay.ApiClient/Store/Return",
    "BackendURL": "http://10.11.22.113:8803/QPay.ApiClient/AutoPush/PushSuccess"
}
  1. Hash ID (AES Key)
87282A2FA0E209EBE1B3713AB56A06C2
  1. IV值
    由於IV值是每次都要从Nonce取用得来,因此这个值会是每次都不同的。

永丰提供了一个加解密测试页面,我们就以使用测试网页产生的值作为目标值,设法撰写Python程序一步步达到和上面一模一样的Message内文。
测试页面:
https://sandbox.sinopac.com/QPay.ApiClient/Calc/Encrypt

https://ithelp.ithome.com.tw/upload/images/20210919/20130354OV3RynyaO5.png

https://ithelp.ithome.com.tw/upload/images/20210919/20130354RA41tmGv9f.png

首先会需要使用Python的AES功能前,要先确认是否有安装PyCryptodome套件。
相关用法可参考官方文件:
https://pycryptodome.readthedocs.io/en/latest/src/cipher/classic.html#cbc-mode

Python Code如下:

import json
from Crypto.Cipher import AES 
from Crypto.Util.Padding import pad

shop_data = {"ShopNo":"NA0001_001","OrderNo":"201807111119291750","Amount":50000,"CurrencyID":"TWD","PayType":"C","ATMParam":{},"CardParam":{"AutoBilling":"N","ExpMinutes":30},"PrdtName":"信用卡订单","ReturnURL":"http://10.11.22.113:8803/QPay.ApiClient-Sandbox/Store/Return","BackendURL":"https://sandbox.sinopac.com/funBIZ.ApiClient/AutoPush/PushSuccess"}

data_string = json.dumps(shop_data, ensure_ascii=False, separators=(',', ':'))

print(data_string)
#output: {"ShopNo":"NA0001_001","OrderNo":"201807111119291750","Amount":50000,"CurrencyID":"TWD","PayType":"C","ATMParam":{},"CardParam":{"AutoBilling":"N","ExpMinutes":30},"PrdtName":"信用卡订单","ReturnURL":"http://10.11.22.113:8803/QPay.ApiClient-Sandbox/Store/Return","BackendURL":"https://sandbox.sinopac.com/funBIZ.ApiClient/AutoPush/PushSuccess"}

key = b"4DA70F5E2D800D50B43ED3B537480C64"
iv = b"346BBE8E3F34FFEA"

cipher = AES.new(key=key, mode=AES.MODE_CBC, iv=iv)
message = cipher.encrypt(pad(bytearray(data_string, 'utf-8'), AES.block_size))

print(message.hex().upper())

#output: 4FE341D3A8C30C9A50573F3008F7B1CA8DD96FB2A4346D83936E5C4FDB21E87BA9E3D36A6635C6F5EBBD5438F3CA8FE97DEBB2ADBC82F92BF3C840B3128D8F00116536E7C936D7D587F6220C52C1367DF2BE9CBB16C6A7C6242AA8B38CD2E576328CF727E50FFA49B4F9FBE5DF10986C5299F9FC26E23E956AFDFB92B731FDA84ABEF1C89E0CD0A8CA8F7C23DC2D06E12A6F916EC47CDD9B4D4F87AC0B687EE1088A19F2C35C0FD8B0C97745B926FBAA48FEEDEB826C2C22743DB46781FF220ECA409FC150908540271E60184729C08C73275C54125C3F814FF33CA79A0E1B3902D446925FCC8235809FCBAB7E372D8C29E424CEFF0AD1CBD41E843714EB365158F2FC0B2E6FB48176D5CFF6B68F4BED4D7484C1A4723ABD059DA64A6703B30B0199B170FDF059899552FA1818ABA5B0D0E21014513985A738D59851EDF0B1CFB36A7B7B727109BE7789D284C75E5D694DFC9B7060DCBFD8C7915C95C4E0F29B

程序说明

由於内文讯息是JSON格式,而Python的Dictionary Object本身就支援了几乎和JSON是相同结构的表示法,因此可先以此方式准备这个内文讯息内容,再将之转换成json格式的字串。

但这边有几点要格外注意:

  1. 使用json.dumps()可直接将Python的Dictionary Object转换成JSON字串
  2. 若直接不带额外参数转换出来的JSON字串,会和永丰测试页面的JSON字串有些许差异(中文会变成ASCII字符显示而非中文,且dumps出来会加上一些空格。这样最後做出来的AES加密结果会不同。

如果我们没有修正,则内文dumps转出後会呈现下面的情况:(注意原本中文的地方,以及多加上的空格)

{"ShopNo": "NA0001_001", "OrderNo": "201807111119291750", "Amount": 50000, "CurrencyID": "TWD", "PayType": "C", "ATMParam": {}, "CardParam": {"AutoBilling": "N", "ExpMinutes": 30}, "PrdtName": "\u4fe1\u7528\u5361\u8a02\u55ae", "ReturnURL": "http://10.11.22.113:8803/QPay.ApiClient-Sandbox/Store/Return", "BackendURL": "https://sandbox.sinopac.com/funBIZ.ApiClient/AutoPush/PushSuccess"}

因此我们在json.dumps()中传入两个参数来修正:

  • 使用ensure_ascii=False让字串以非ASCII码呈现
  • 使用separators=(',', ':')重新定义产生出来的JSON分格字符,原本逗号与冒号後会多带一个空格,现在重新定义无空格版本给它。

修正完的结果就会是我们要的了 (无空格、ASCII乱码修正)

{"ShopNo":"NA0001_001","OrderNo":"201807111119291750","Amount":50000,"CurrencyID":"TWD","PayType":"C","ATMParam":{},"CardParam":{"AutoBilling":"N","ExpMinutes":30},"PrdtName":"信用卡订单","ReturnURL":"http://10.11.22.113:8803/QPay.ApiClient-Sandbox/Store/Return","BackendURL":"https://sandbox.sinopac.com/funBIZ.ApiClient/AutoPush/PushSuccess"}

而在AES加密过程时,除了把原明文资料、AES Key、IV的bytearray传入,在cipher.encrypt()加密时要符合区块长度的倍数,AES使用128 bit(16 bytes)的区块长度,因此我们的明文也需要符合其倍数规则,因此可透过Crypto.Util.Padding中的pad()方法来作padding填补,所以在将明文的bytearray取出後可带入AES.block_size作为填补参数。
例如原本内文长度为349 Bytes,经pad()後会自动填补为符合16 Bytes倍数规范的352 Bytes。

终於把Message密文准备好了

经过了这一连串的备料准备後,终於把传说中的Message密文准备好了!
但还没完呢,除了我们准备好的密文外,还有一个重要的安全签章Sign要准备,才可以把最终呼叫API的所需栏位备齐。


<<:  模板中的 Directive 指令 (下)

>>:  Day07 - Gem-sidekiq-limit_fetch 限制 sidekiq queue 执行数量

【Day14】verilog 中的可综合语句

我们都知道 verilog 是一种硬体描述语言,所以目的就是要能综合出实际的电路,但实际上在 ve...

【Day31】新加坡工作後续的时程

Update(01/10):EP approved! 在这边纪录一下这份 offer 的各个时程。...

Day 29:653. Two Sum IV - Input is a BST

今日题目 题目连结:653. Two Sum IV - Input is a BST 题目主题:Ha...

日记18

推荐前端工程师好文章 https://ithelp.ithome.com.tw/m/users/20...

Day14_附录A.控制项(A8_资产管理)

如果只有贴图上来,应该还是会很想打死我,就打死我吧,卡壳严重冏rz。 ▉附录A的A8资产管理,以资产...