加密問題(智付寶)


#1

剛剛試著串接智付寶一直失敗… 求救

智付寶API文件流程是

“交易資料拼成的字串” => AES加密

“加密後的字串,前面加上hash_key後面加上hash_iv” => SHA256二次加密

兩次加密的結果都要送過去

然後一直 “交易資料 SHA 256 檢查不符合”

文件的範例是

AES加密
Key = "12345678901234567890123456789012"
IV = “1234567890123456”

加密前的字串 = “MerchantID=PG300000000055&TimeStamp=1489630207&Version=1.0&MerchantOrderNo=S_1489630207&Amt=30&ItemDesc=UnitTest”

AES加密後字串=
"89931dedfbc62460c637791dde28cfa465d13c5141dca0e7c5ab75bc66c9d459b6eeb4f0
71afed1bb41cfc9b25613ae39724e6e3fe7d9223a181c29e580257802b815cc09263032d
2c804446542a0915d0abc05f9d63072d4397b6e6a81cbd2bd3ac04965755d85c7038a3c
1838d031a20bd98173b6e57949a48003dffa5521a "

不過我做AES加密一直沒辦法得到跟文件一樣的結果…

下面是我的code

def test_encrypt
  str = "MerchantID=PG300000000055&TimeStamp=1489630207&Version=1.0&MerchantOrderNo=S_1489630207&Amt=30&ItemDesc=UnitTest"
  cipher = OpenSSL::Cipher::AES.new(128, :CBC)
  cipher.encrypt
  cipher.key = "12345678901234567890123456789012"
  cipher.iv  = "1234567890123456"
  encrypted = cipher.update(str) << cipher.final
end

這樣寫會噴錯誤 incompatible character encodings: ASCII-8BIT and UTF-8
加密後的東西長下面這樣
"\xB3v\xE1\xF8\xBC\x9C&A\b=H\r\xF4\xF9>\x13+\x12\xE5\x975\x85m}9\xCF\xDEp\xC5b\x00s\xD1\x1A\xC9<\x94^H&\xB8\x84!\xC4\xB8\xAA\x16bB\rB\x1C\xEA\xA3\x9CA\xF4\x85\xA8a\xE5\xD2\xC5\xE7\xCE\xDD;|+\xE6]\xEDvV\xF4\xB2M\x96:\xEC\x18"\xAC\xD0\xB9\x82\xCF\xD7\t \xD0a\xDD\xCF\x14^;u\xB2\n\x9DYP\xE7^*U\xF9Ko\x13\xEC\xB2\x9A<7\x04\xAEG\xD4\x82\xBE(\xDA4K\xD3\x18"

把它轉成Base64編碼的話

def test_encrypt
  str = "MerchantID=PG300000000055&TimeStamp=1489630207&Version=1.0&MerchantOrderNo=S_1489630207&Amt=30&ItemDesc=UnitTest"
  cipher = OpenSSL::Cipher::AES.new(128, :CBC)
  cipher.encrypt
  cipher.key = "12345678901234567890123456789012"
  cipher.iv  = "1234567890123456"
  encrypted = cipher.update(str) << cipher.final
  encoded = Base64.strict_encode64(encrypted)
end

會長這樣
"s3bh+LycJkEIPUgN9Pk+EysS5Zc1hW19Oc/ecMViAHPRGsk8lF5IJriEIcS4qhZiQg1CHOqjnEH0hahh5dLF587dO3wr5l3tdlb0sk2WOuwYIqzQuYLP1wkg0GHdzxReO3WyCp1ZUOdeKlX5S28T7LKaPDcErkfUgr4o2jRL0xg="

想請問要怎麼做AES加密才能得到跟文件一樣的結果呢?
官方api網址 https://www.pay2go.com/dw_files/api_files/API_E_wallet_P2G_1.0.0.pdf
第25頁

萬分感謝~


#2

嗯,我剛試了一次,發覺他們的 example 答案應該有誤,而我找到了別人的包裝出來的結果和我的完全一樣,所以直接送看看唄(其實你也可以直接用下面那個包裝過的 gem 即可?哈哈)

以下是我的寫法,語法要照抄的話記得任何一個環節都不能少,類似那個 add_padding 的 method

require "openssl"

def add_padding(source, size = 32)
  len = source.length
  pad = size - (len % size)
  return source + (pad.chr * pad)
end

source = 'MerchantID=PG300000000055&TimeStamp=1489630207&Version=1.0&MerchantOrderNo=S_1489630207&Amt=30&ItemDesc=UnitTest'
cipher = OpenSSL::Cipher::Cipher.new("AES-128-CBC")
cipher.encrypt
cipher.key = "12345678901234567890123456789012"
cipher.iv  = "1234567890123456"
encode = (cipher.update(addpadding(source)) + cipher.final).unpack('H*')[0]

#=> "b376e1f8bc9c2641083d480df4f93e132b12e59735856d7d39cfde70c5620073d11ac93c945e4826b88421c4b8aa1662420d421ceaa39c41f485a861e5d2c5e7cedd3b7c2be65ded7656f4b24d963aec1822acd0b982cfd70920d061ddcf145e3b75b20a9d5950e75e2a55f94b6f13ecb29a3c3704ae47d482be28da344bd318308569170f07115d91d11a636de99dd4"

所有過程都按照原 code 演算法 1:1 打出來的,所以沒理由會錯就是,且找了第三方 gem 驗證是一樣的輸出了,so~

這邊提幾點,人家要的是 hex 而非 base64,所以你不應該用 Base64,而是要用 bin <=> hex 的方式來轉換,而 Ruby 這類轉換最快的方式是 pack & unpack

"你好嗎".unpack('H*')[0]
#=> "e4bda0e5a5bde5978e"
["你好嗎".unpack('H*')[0]].pack('H*')
puts ["你好嗎".unpack('H*')[0]].pack('H*')

["你好嗎".unpack('H*')[0]].pack('H*') == "你好嗎"
#=> false
["你好嗎".unpack('H*')[0]].pack('H*').force_encoding('UTF-8') == "你好嗎"
#=> true

缺點是 String 的 encoding head 會在轉換中遺失(因為轉的只有原始內容而已…),所以要用 force_encoding 轉回去即可,以上


#3

感謝JC

用hex直接送過去就過了,example好像真的有問題

本來用base64,在解回來,然後壓SHA送過去一直過不了 哈哈哈


#4

又遇到奇怪的問題了

送出去後測試環境下交易成功了

然後智付寶會回傳 Status 以及 TradeInfo,回傳的TradeInfo也是被AES加密過的

收到的Status是SUCCESS

TradeInfo解密後卻有點奇怪

長下面這樣

?H5#<L1xs431qqSS",“Message”:"\u4ea4\u6613\u5b8c\u6210",“Result”:{“MerchantID”:“PG100000002345”,“Amt”:“300”,“TradeNo”:“17062300431552543”,“MerchantOrderNo”:“1706230042589D52F36A”,“PaymentType”:“WEBATM”,“PayTime”:“2017-06-23 00:43:15”,“IP”:“114.41.68.161”,“EscrowBank”:“HNCB”,“PayerAccount5Code”:“12345”}}

照文件講的話 收到的 status為 success的話

TradeInfo解密後開頭應該是 {“Status”:“SUCCESS”

文件範例
{“Status”:“SUCCESS”,“Message”:"\u6388\u6b0a\u6210\u529f",“Result”:{“MerchantID”:“PG300000000055”,“Amt”:“30”,“TradeNo”:“17031709394299741”,“MerchantOrderNo”:“S_1489714728”,“PaymentType”:“CREDIT”,“PayTime”:“2017-03-
17_09:39:42”,“IP”:“59_124_92_194”,“EscrowBank”:“HNCB”,“RespondCode”:“00”,“Card6No”:“400022”,“Card4No”:“2222”,“Inst”:“0”,“InstFirst”:30,“InstEach”:0,“ECI”:"",“Exp”:“2005”,“Auth”:“930637”,“AuthDate”:“20170317”,“AuthTime”:“093942”,“AuthBank”:“NCCC”}}

除了開頭外後面都滿正常的


#5

人家加密有 padding,解密也要有 padding 的啊 … 怎樣加密就會有怎樣解密,兩個成對的不是?
AES 系列是分段加密的,每一塊 = key 的長度,所以長度不夠都要 padding,分別是 OpenSSL 內建幫你填空還是你自己自訂,而加密後 lib 幫你接在一起而已,所以你的 padding 有問題時,就會噴某一塊之類的,通常都在頭尾哩

所以我會建議你用 PHP 的 code 的順序逆寫回 Ruby 會好點,而事實上我也很常把別的語言改寫成 Ruby 就是

不過話說回來,如果你已經有 padding 了,那就拿掉看看唄,可能是你自己 padding 錯邊…結果…(把尾巴弄到頭去了),anyway 這邊都要自己嘗試就是


#6

感謝JC

因為不懂PHP,文件上的PHP範例都被我略過…

最後發現我犯蠢,解密時忘了給iv難怪padding怎麼弄都不對…

加密解密編碼這塊功力太差需要補洞了

剛剛稍微看了一下AES的原理不知道理解的對不對

AES的部分,假設 key 長度為 32字元

要加密的字串如果長度是 50 ,那就必須補滿到 32 的倍數,是這樣嗎

看要補前面OR後面
ex:

欲加密字串 = “a” x 62

智付寶的padding方式做padding後

“aaaaaaa…共62個a\u0002\u0002” ,共64字元

所以接收方除了要有KEY之外還必須知道發送方padding的補法,才能解出來

下面是他們提供的php解密code,

function aes_decrypt ($aes_str, $key, $iv) {
	$aes_str = str_replace(' ', '+', $aes_str);
	$str = strippadding(mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key,
	hex2bin($aes_str), MCRYPT_MODE_CBC, $iv));
	return $str;
}
function strippadding($string) {
	$slast = ord(substr($string, -1));
	$slastc = chr($slast);
	$pcheck = substr($string, -$slast);
	if (preg_match("/$slastc{" . $slast . "}/", $string)) {
		$string = substr($string, 0, strlen($string) - $slast);
		return $string;
	} else {
 		return false;
	}
}

strippadding這個拿掉padding的函數是解密後才執行的(應該是吧?)

本來想法是,智付寶加密前已經做過padding,資料字元數能被32整除

解密完會大概長這樣 “想要的data” + “填補的字串”

所以先設定padding = 0,然後解密,再篩選想要的字串

搞半天才發現iv一直沒給… 給好iv後開頭就不是亂碼了

OpenSSL 解密完自動去掉padding後內容是正確的

是不是表示OpenSSL內建的解密method

加入padding跟移除padding的實現方式跟智付寶是一樣的?


#7

key 的長度基本上就是內文長度,而 padding 或是用哪種方式來 padding 這邊就是爭議點了(你的東西沒做 checksum 所以沒差哩,反正對方能解開就好了,通常都填空白而已),and 正確就好,其他不用考慮太多哩

之前一個專案一樣是 AES + SSL 專簽 + checksum 在 header 內,裡面一個 byte 都不能錯才頭大,且內容裡面有 timestamp,每次結果一定都不一樣,要和對方對也沒辦法對,那是我搞最久的一次 Orz" (對方還會靠北說:這是金融業基本格式 … 我他喵的用的是 OpenSource 哩,只提供暈到死和專用 Java SDK 很想掐死他們 … )所以你這類的能動對我而言就 ok 哩,其餘就經驗多做幾個金流玩玩看而已