Day11 - [丰收款] 礼尚往来,我们也需要解开API回传的密秘

回顾昨天拿到的response,乍看之下以为API将我们传给他的内容原封不动传回来了。
但实际上比对一下内容就会发现,API也给了我们一串我们需自行解开的密文。

其中有三个关键:

  • Nonce
  • Sign
  • Message

请睁大眼睛看一下,这个Nonce和我们传给他的Nonce不一样,所以千万不要拿自己刚那个Nonce自作聪明的往下作。

在我们经过这麽多天的特训後,应该闭着眼睛都可知道如何解开这个密文,以及验证这个密文是由永丰API正确无误无遭人窜改的内容吧!

#-- Response: {'Version': '1.0.0', 'ShopNo': 'NA0249_001', 'APIService': 'OrderCreate', 'Sign': '8A001F83ABF5EAF292119ADBFDBCFE7F34A535781E8F77A7B0D09A9FD56E90BF', 'Nonce': 'NjM3NjgxMDcxMzg2OTUuMTo4MTQwODE2NzBiMGUyYTdiNTAzZDExN2Q5NDhmOGMzMTVlZWRhOGI5ODY2OGUyOGNkMGFiM2MzZDhiNGEzZGRi', 'Message': '4AAC75A87C46EC473D94FFFB270DCAF3263CC5DB5F3E49ABCEE8E28A073F16D750469AEE4E77A1F0237DEA7043CD79273E0300D94286C81DF70B4A2C2BEA54DDA7AE4F137D109E9E6FBF4494FDCA9749C61F1DD9A30CFC7A831735D5811B26FAABC23B7C1E6CD7329974AE866EC2A72F09574E2A0C334A8F227FFF1462489E8187CCE9986940272C7B7BB1A676171F898D03909CD96EA6981B6EA7CB02003ED4DC1D95190F76DCB071E4BEDDDB55BB4D1EC7B06681D0FA583051112DDC36B1A3459A14C28789E5EBF02451EC77AC0F0DDBE00D2B07FF0D7BE195E866AF3D341CC21E8C346D2A72C4541898595F81AB60894049A32A5C551C91E4F492EF3F33F32268A8EDAB1AFBAA49F6ED1833BFD756F1955FA6BB1A3FC38773FE42E53DA5B82911073356C3A2211DE51810C5CDB54E73FFCC67BA0441BB7F53BCB4D640BD73F06336BE1FEA4A0ACFA316F07F0A5FE232380CBF245AF01777BBDF770EDD08F77F853BDC2715FDA066F271C58F31424C47B1593829E7D67A5105224AEBF10D99DB2CFC9F6483440601DAACFB20251D724DFD8447C0A28408921966A3084E97C564017973A9B8CED71F00371F391663196D8021CEF2B74C86AF0EACD275A5BFE8F2D1B787648F64EF2CCEB4CB8834B8E1'}
def aes_dec(data_string, resp_nonce):
    hash_id_ba = hash_id.encode("utf-8")    
    iv_ba = get_aes_iv(resp_nonce).encode("utf-8")
    cipher = AES.new(key=hash_id_ba, mode=AES.MODE_CBC, iv=iv_ba)

    message = bytes.decode(unpad(cipher.decrypt(bytes.fromhex(data_string)), AES.block_size), "utf-8")
    return message

resp_nonce = resp["Nonce"]
resp_msg = resp["Message"]
resp_ori_sign = resp["Sign"]

dec = aes_dec(resp_msg, resp_nonce)
print("- Decryption of Response: {}".format(dec))

#{"OrderNo":"A202109838256","ShopNo":"NA0249_001","TSNo":"NA024900000227","Amount":79900,"Status":"S","Description":"S0000 – 处理成功","PayType":"A","ATMParam":{"AtmPayNo":"99922530174963","WebAtmURL":"https://sandbox.sinopac.com/QPay.WebPaySite/Bridge/PayWebATM?TD=NA024900000227&TK=82cd04db-cd70-4bf8-8215-73675e920fd9","OtpURL":"https://sandbox.sinopac.com/QPay.WebPaySite/Bridge/PayOTP?TD=NA024900000227&TK=82cd04db-cd70-4bf8-8215-73675e920fd9"}}	

程序说明

我们取回来的值,会先拆开第一层的json,Version、ShopNo、APIService原则上会和我们呼叫时的内容是一样的。而我们要先将Message值取出来要做AES解密,而Sign值取出来要做比对验证确认其内容不可否认性(non-repudiation)。而Nonce值是要重新产生这次解密的IV值的基础。

我们要撰写一个AES-CBC的解密aes_dec(),其实内容和加密差不多。将取回来的Message密文和Nonce传入後,使用AES的decrypt()将结果解出来,由於双方采用的AES是对称式加密,因此我们手上的AES Key,就是我们先前的Hash ID。成功解密後,我们就会拿回人类看的懂的第二阶的JSON内容。

记得当我们在加密时有做过pad()的padding手法,一样的我们在解密时也需要做反向的unpad(),把原本有padding的值再拿掉,否则有时候解回原文时最後在尾巴会产生乱码。

接下来,我们就要把JSON内容再拿出讯息内文来重新计算Sign的内容。

resp_json = json.loads(dec)

resp_gen_sign = get_sign(resp_json, hash_id, resp_nonce)

print("- 重新产生Sign值: {}".format(resp_gen_sign))
# Output: - 重新产生Sign值: 52BA786E4E6BBE5DB5A41FF8B656565EB529D135B276BFC3D17D0BB9467F4B4C

print("- Sign验证结果,是否样同? {}".format(resp_ori_sign == resp_gen_sign ))
# Output: - Sign验证结果,是否样同? True
程序说明

原本我们解密回来的是一个JSON字串,所以要把字串经由json.loads()转成Python Dictionary。这一整串就是讯息内文,加上Hash ID以及新取回的Nonce,规则和先前是一样的,因此就重覆使用我们撰写好的get_sign()进行计算,会得到新的Sign值内容:52BA786E4E6BBE5DB5A41FF8B656565EB529D135B276BFC3D17D0BB9467F4B4C

我们立刻把这一串安全签章拿去和API回传给我们的比对一下,完全相同

需要把这一个验证步骤也做完後,才算是完整的流程,但开心之余,我们是不是忘了什麽?

最重要的事

还有一件最重要的事,当然就是要解析API回传给我们的JSON内容:

  • 确认状态Status:要收到S才代表成功
  • 确认状态描述内容Description:若是成功的话,会是S0000 – 处理成功
  • 永丰端的交易序号TSNo:我们的例子比拿到了NA024900000227
  • 虚拟帐号相关资讯ATMParam
    • 最重要的在这儿里呀,本集最重要的主角:AtmPayNo,如果顾客不是选择使用WebATM网页上转帐的话,可在电商的页面上显示这个虚拟帐号让他们使用惯用的方法转帐。我们的例子拿到了99922530174963
    • 永丰也提供了线上WebATM的方式缴款,我们只需要提供连结即可,即WebAtmURL
    • 另一个是使用永丰银行简讯动态密码(OTP)的付款网址,即OtpURL

把值从Dictionary中取出即可,这部份很简单:

tsno = resp_json["TSNo"]
print(tsno)
# Output: NA024900000227

status = resp_json["Status"]
print(status)
# Output: S

desc = resp_json["Description"]
print(desc)
# Output: S0000 – 处理成功

atm_param = resp_json["ATMParam"]

atm_pay_no = atm_param["AtmPayNo"]
print(atm_pay_no)
# Output: 99922530174963

web_atm_url = atm_param["WebAtmURL"]
print(web_atm_url)
# Output: https://sandbox.sinopac.com/QPay.WebPaySite/Bridge/PayWebATM?TD=NA024900000227&TK=82cd04db-cd70-4bf8-8215-73675e920fd9

otp_url = atm_param["OtpURL"]
print(otp_url)
# Output: https://sandbox.sinopac.com/QPay.WebPaySite/Bridge/PayOTP?TD=NA024900000227&TK=82cd04db-cd70-4bf8-8215-73675e920fd9

有关PayToken

在规格书有提到:

丰收款会依 BackendURL 或 ReturnURL 将讯息 内 Token 传送给商户,商 户会收到一组 Token 值後使用「 6.5讯息查询服务」来确认内容...

但由於使用虚拟帐户的要求时,ReturnURL必填,但BackendURL并不是。其实我不是很确定ReturnURL会在什麽情况下被用到。我先假设是使用永丰的WebATM或OTP的服务时,毕竟是在永丰的网站作业,而连过去的网址带了一些资讯应该可让永丰後台mapping到我们这笔交易资料,也理当在执行完付款动作後,就可以将使用者转址回我们当初提供的ReturnURL的网址中。

但若顾客并没有想要使用永丰的WebATM或OTP时,表示顾客想记下虚拟帐户,使用其他的转帐方法来完成支付动作。这样一来,接下来的付款流程就和永丰可控的网站是脱钩的状态,因此ReturnURL似乎就没有机会被叫用了。
那这样一来,当初非必填的BackendURL就似乎变的至关重要了,因为这变成是在这个情境下唯一能取得PayToken的机会。

需要有PayToken,我们才能使用OrderPayQuery来查询订单的付款状态,需要能查询我们才能在电商的订单後台中,更新付款状态让客户确认。想像一下如果你是顾客,付完款後,一定会想要确认网站的状态是否更新成「已付款」,才会安心。

我试图想使用WebAtmURL来看看完成後,是否会进行转址。但我从取回的网址连线後,发现这个测试网页是无法使用的,画面如下:
https://ithelp.ithome.com.tw/upload/images/20210924/20130354KyNDmGQfGC.png

目前还没有办法实现被转入ReturnURL,而且这个转址虽然是Client Side转址,但转过去後取得网址列的参数(主要是要拿PayToken)後,也是需要透过Server Side程序去处理与储存,而不是靠顾客的Browser的前端程序。因此也是想找时间实作一个我方的BackendURL 让永指API可回报PayToken


<<:  [Day18] swift & kotlin 实作篇!(9) Animation -kotlin

>>:  【Day 11】C 语言的赋值运算子

005-元件名称_2

关於上篇提到的元件,对我而言,属於在讨论阶段,会比较经常拿出来讨论的元件。真正在实作以及管理画面时,...

Day17 Android - Array、ArrayList、List

今天主要来提提Array、ArrayList、List其中一些不同的地方及概念,那麽首先先提提有关於...

Day 30 Quantum Protocols and Quantum Algorithms

Solving Linear Systems of Equations using HHL HHL ...

Day18 X Service Workers Cache

如果你听过 PWA,那麽对今天的主题ㄧ定不陌生,因为今天要讲的 Service Worker 就是...

Day16:终於要进去新手村了-Javascript-回圈-while

回圈有两种语法可以使用,分别是while与for回圈,今天这篇会先来讲到while回圈的部分。 基本...