一次debug到哭泣的經歷。
龍雲堯個人博客,轉載請註明出處。
CSDN地址:http://blog.csdn.net/Michael753951/article/details/70481546
個人blog地址:http://yaoyl.cn/rfidshi-yan-san-zong-jie/
在實驗過程中,需要不斷翻閱實驗課PPT之《04 電子錢包的功能》,word之《實驗3文檔》,CSDN大佬呂浪的課程總代碼以及他的的Java card開發系列文章。
本次實驗和前兩次實驗相比,代碼量多很多,並且實驗思路稍有區別。實驗之前可以不太懂實驗流程(主要是因爲流程本身就太複雜了),但是一定要一遍又一遍閱讀源代碼,只有在讀源碼的過程中,才能體會整個驗證過程,對項目中涉及到的函數方法的使用纔能有一個更加深入的瞭解。接着自己不斷重寫代碼,理解整個實現過程,才能對這個課程實驗有較爲深入的瞭解。
代碼在未徵得本人同意之前,請勿直接Ctrl+C,Ctrl+V,謝謝。
正式實驗
實驗分析
首先我們在PPT中知道本次實驗的主要需要實現的功能是:
- 圈存
- 消費
- 餘額查詢
接下來我們開始看ppt《04 電子錢包的功能》和《實驗3文檔》。
首先是圈存功能的流程圖。
流程圖中我們可以分析出圈存一共有4個步驟:
- 終端發送消息初始化
- IC響應初始化,並且發送MAC1驗證
- 終端驗證MAC1,確認IC卡是否合法,然後發送包含MAC2的圈存命令
- IC卡驗證終端機的合法性,執行完成以後返回TAC響應操作完成
接下來我們將一步一步仔細分析圈存是如何實現的。
step1:圈存機發送的初始信息如下所示。消息中包含了祕鑰標識符,交易金額,終端機編號。
step2:
- IC卡根據祕鑰標識符尋找圈存祕鑰
- 生成過程祕鑰。輸入數據爲[僞隨機數||電子錢包聯機交易序號||8000],祕鑰爲圈存祕鑰,使用3DES加密算法。
- 生成MAC1。輸入數據爲[電子錢包餘額(交易前)||交易金額||交易類型標識||終端機編號],祕鑰爲過程祕鑰,使用我們在上一次實現的MAC生成函數gmac4,計算出MAC1用來表明身份。
- IC卡返回[餘額||聯機交易序列號||祕鑰版本號||算法標識||僞隨機數||MAC1]。
step3:
- 圈存機對IC卡發揮的MAC1信息進行校驗,如果正確就說明IC卡信息合法。
- 計算MAC2。輸入信息爲[交易金額||交易類型標識||終端機編號||交易日期(主機)||交易時間(主機)],祕鑰爲過程祕鑰,加密算法爲依然爲gmac4。用來表明自己的身份。
- 發送圈存指令。消息中包含[交易日期||交易時間||MAC2]。
step4:IC使用同樣的算法計算MAC2,如果計算結果和終端返回的MAC2一致,就說明終端的身份合法。IC卡就會執行圈存命令。同時返回TAC。其中TAC計算時,輸入數據爲[電子錢包餘額(交易後)||電子錢包聯機交易序號(加1前)||交易金額||交易類型標識||終端機編號||交易日期(主機)||交易時間(主機)],密鑰爲TAC密碼最左8個字節與TAC密碼最右8個字節異或的結果。
到這裏整個圈存過程就結束了。消費以及查詢和圈存的實現原理一致,這裏就不贅述了。
讀代碼
前面的分析中,我們已經對本次實驗有了大致的瞭解,接下來就是開始讀源碼的過程了。不過本次實驗中,因爲我們只需要對IC卡的系統進行編程實現,而對終端機需要靠人腦完成,所以我們重心就會放在圈存的初始化和圈存的執行上面了。
圈存初始化
還是老樣子,我們先讀項目給的源碼中的Purse
部分,裏面有圈存初始化和圈存確認信息的處理函數。
在TA給的源代碼中,init_load
和load
方法是已經給好了的,我們先讀這兩部分的源代碼,理解整個設計思路。
首先我們需要修改Purse類,讓其process
方法裏面增執行圈存初始化,圈存,消費初始化,消費,以及查詢的入口。
因爲圈存和消費的init方法的ins都一樣,所以我們還需要增加一個判斷方法,利用兩者p1參數不一樣,來判斷是init_load
還是init _purchase
。
好了,入口寫好了。我們開始看init_load
的實現方法。
前面還是和讀寫數據時一樣的操作,進行參數校對,這個部分已經很簡單了。我們看到有一個findkey
方法。點進去查看函數的具體內容。
查找方法比較簡單,從Key數組中獲取祕鑰,存進pbuf中,如果存在就返回祕鑰標識符,否則返回0。
回到init_load
,在執行fendkey
之後,是異常處理,驗證是否查找失敗。然後執行init4load
方法。繼續點進去查看。(前方高能預警)
回憶一下初始化的過程中,IC的操作部分是怎麼操作的。
- IC卡根據祕鑰標識符尋找圈存祕鑰
- 生成過程祕鑰。輸入數據爲[僞隨機數||電子錢包聯機交易序號||8000],祕鑰爲圈存祕鑰,使用3DES加密算法。
- 生成MAC1。輸入數據爲[電子錢包餘額(交易前)||交易金額||交易類型標識||終端機編號],祕鑰爲過程祕鑰,使用我們在上一次實現的MAC生成函數gmac4,計算出MAC1用來表明身份。
- IC卡返回[餘額||聯機交易序列號||祕鑰版本號||算法標識||僞隨機數||MAC1]。
按照這個思路看源代碼。(看代碼的時候一定要對着ppt的流程讀,我第一次讀源代碼就是單純對着IDE讀,結果讀的很爽,但是讀完了只知道每個基本操作在幹嘛,但是整個操作流程還是一臉懵逼。)
開始看代碼。
首先從data
中提取交易金額,終端編號,存進pTemp42
和pTemp81
。
然後判斷是否超額。我們繼續點進去看一下increase
的源代碼。
從EP_ balance
中和data
中依次取出一個字節,將其相加再和一個ads
(進位標誌符)相加,如果flag
爲真,就改寫EP _balance
中的值,然後更新ads
。最終返回進位標誌位ads
。整個就是大數加法的思想方法。但是EP _balance
是啥???我們再點進去看源代碼。
是當前電子錢包的餘額。因此整個increase
就是判斷當前餘額加上一個圈存值,如果超額(結果超過4bytes),就會返回1(那個進位標誌位ads
)。否則返回0。如果傳入的第二個參數爲false,就不會更新餘額,否則會執行餘額更新操作。
好,到這裏我們知道了rc = increase(pTemp42, false);
部分的意義了。再往下看密碼取位部分。
首先使用readkey
方法。點進去查看。
這裏需要對Key
的結構有一定的瞭解了。我們點進去查看Key
這個類定義和實現。
所以Key
中存放的結構爲2bytes信息位(分別爲1byte的pbuf
,1byte的length
),5bytes的祕鑰頭,以及16bytes的祕鑰值。其中length是addKey傳入value的長度,爲表頭+祕鑰的長度。
回到readkey
,readkey
就是將祕鑰取出來,然後將祕鑰表頭中的value
值取出來(5bytes表頭+16bytes祕鑰),返回長度爲祕鑰的實際長度(減掉了表頭長度)。
回到init4load,所以這4行代碼的意義就是
- 按照祕鑰獲取部分就是按照祕鑰標識符獲取bytes祕鑰頭+16bytes祕鑰存進pTemp32
- 從祕鑰頭第4個bytes獲取祕鑰版本號,從第5個bytes獲取算法標識符
- 從獲取pTemp32中將祕鑰的實際值(從第5位開始讀取祕鑰長度個bytes),取出來存進pTemp16。
再往下讀隨機數產生的代碼。
調用rd.generateData
方法,對傳入的參數進行操作。我們在查看generateData的實現方法的時候,已經進入.class文件,沒有發現有意義的信息。於是折返查看v和size的信息。
根據註釋,我們可以大概知道GenerateSecureRnd
的意義就是根據特定的隨機數產生機制產生隨機數,然後寫進v。
getRndValue
方法就是將隨機數v寫入參數bf中,偏移位爲bOff。
回到init4load
,產生隨機數這兩行的意義就是產生隨機數,然後將隨機數寫進pTemp32
的[0:3]位。
接下來,將EP_online
(電子錢包脫機交易序號,之前分析圖片中出現過)寫入pTemp32[4:5]
,將0x8000
寫入pTemp32[6:7]
,調用上一次實驗中實現的3des
加密算法,祕鑰爲上面得到的圈存祕鑰(存在pTemp16
中)產生過程祕鑰,寫入pTemp82
。
回看前面分析的圈存初始化的第一步和第二步,①IC卡根據祕鑰標識符尋找圈存祕鑰;②生成過程祕鑰。輸入數據爲[僞隨機數||電子錢包聯機交易序號||8000],祕鑰爲圈存祕鑰,使用3DES加密算法。是不是一模一樣?
好,我們再往下看。
產生MAC1
。首先分別往pTemp32
中寫入EP_balance
(餘額),data[1:4]
(交易金融,對着那一頁PPT找data
結構就知道了),0x02,data[5:10](
終端機編號,一樣看ppt中的data
結構),然後將pTemp32
中的內容複製到data
(不知道這裏寫入data有什麼意義,因爲在響應數據部分又會被寫一次。)。然後使用上次實驗實現的gmac4
,輸入數據爲pTemp32
,祕鑰爲上一步得到的存在pTemp82
的過程祕鑰,得到的mac1
存在pTemp41
中。
同樣回看前面分析的圈存初始化的第三步。生成MAC1。輸入數據爲[電子錢包餘額(交易前)||交易金額||交易類型標識||終端機編號],祕鑰爲過程祕鑰,使用我們在上一次實現的MAC生成函數gmac4,計算出MAC1用來表明身份。是不是對上了?
然後將EP_balance
(餘額),EP _online
(電子錢包脫機交易序號),keyID
(祕鑰版本號),algID
(算法標識符),隨機數,以及mac1寫進data。
回看圈存初始化的第四步,IC卡返回[餘額||聯機交易序列號||祕鑰版本號||算法標識||僞隨機數||MAC1]。一模一樣。
到這裏init4load
也結束了,往上回到init_load
,papdu.pdata
已經在init4load
中設置完成,在papdu.le
(理想的下一次指令中的數據長度)寫爲0x10
。然後purse.init _load
結束。
光分析這一步我寫了一個半鍾,當時在讀源代碼的時候花的時間更久。但是這個步驟不能跳過,它讓我們對整個IC卡的業務邏輯和方法實現以及調用打下了基礎。我們在實現其他方法的時候,才能更加得心應手。
再看purse.load
方法吧。這一步實現了圈存。
前面的就不詳說了,它調用了EPfile.load
方法,我們點進去查看。
回想在一開始分析的時候,step4中,IC卡對接收到的終端機指令是怎麼處理的。
step4:IC使用同樣的算法計算MAC2,如果計算結果和終端返回的MAC2一致,就說明終端的身份合法。IC卡就會執行圈存命令。同時返回TAC。
查看MAC2的產生方法:輸入信息爲[交易金額||交易類型標識||終端機編號||交易日期(主機)||交易時間(主機)],祕鑰爲過程祕鑰,加密算法爲爲gmac4。
查看TAC的產生方法:輸入數據爲[電子錢包餘額(交易後)||電子錢包聯機交易序號(加1前)||交易金額||交易類型標識||終端機編號||交易日期(主機)||交易時間(主機)],密鑰爲TAC密碼最左8個字節與TAC密碼最右8個字節異或的結果。
通過源代碼和步驟分析我們可以得出,EPFile.load
整個過程就是將step4實現的過程,不過需要注意的是,EPFile.load
中不少參數使用的是EPFile.init4load
中存下來放在pTemp
中的值,這也就從邏輯上說明爲什麼我們在執行消費命令前必須執行消費初始化命令,保證了安全性。返回purse.load
,校驗異常,設置papdu.le
,purse.load
也就結束了。
整個閱讀過程需要不斷跳轉代碼,也需要不斷在代碼和PPT《04 電子錢包的功能》和word《實驗3文檔》。同時因爲pTemp實在太多,我們最好能夠在一旁做筆記,記錄下來每一個pTemp存放了哪些值,以及它們的作用,這樣我們才能在實現消費和查詢的時候,使用起來更加方便。
附上我記錄的在init4load和load中各種變量的變化情況以及pTemp們的存在意義。
大部分pTemp其實是有固定意義的(結合PPT中傳入的各種數據長度,我們就不難理解爲什麼了)。pTemp32一般作爲中間變量,用來進行3des加密或者gmac加密。
謹記這一點,結合我們在上一步中總結下來的經驗。我們就可以着手實現init_purchase和purchase以及get _balance了。
查看消費的流程圖。
我們可以總結出消費的如下流程
- 終端發送消息初始化
- IC響應初始化,發回隨機數
- 終端產生MAC1,證明自己的身份,將交易信息發給IC卡
- IC卡驗證終端機的合法性,計算MAC2和TAC,返回給終端作爲身份憑證和消費憑證
然後一步一步詳細分析。
step1:圈存機發送的初始信息如下所示。消息中包含了密鑰標識符,交易金額,終端機編號。(整個消息串中,除了p1、le對比圈存初始化指令有區別以外,並沒有其他區別)。
step2:
- IC卡根據祕鑰標識符尋找圈存祕鑰
- 生成隨機數
- 檢查餘額是否足夠支付本次交易
- 返回[餘額||脫機交易序號||透支限額||祕鑰版本號||算法標識||僞隨機數]
step3: 終端將命令響應數據傳送給主機,主機利用消費主密鑰產生消費子密鑰,並生成MAC1。然後終端向IC發送[交易序列號||交易日期||交易時間||MAC1]。(和圈存中發送的信息中多了一個交易序列號)。
step4:
- IC卡根據消費祕鑰生成過程祕鑰
- 利用過程祕鑰生成MAC1
- 交易序列號+1,將消費金額從卡中扣除。
- IC卡生成MAC2(證明自己身份),和TAC(證明工作完成)。
- 返回TAC和MAC2給終端
首先我們填purse
中的init_purchase
函數。(這一步可以先從init _load
中複製下來,然後稍微修改一下p1
和le
的值,以及調用的函數改成init4purchase
,原因我剛剛在step1
中說過了)。
接着我們需要實現EPFile.init4purchase
。同樣的,我們只需要按照step2步驟,從init4load
中,按需複製代碼就行。
同時我們需要對照並且記錄下來,每一個pTemp使用是否正確,還要記錄init4purchase中每一步執行完成以後pTemp中數據對應的值。
然後我們再實現EPFile.purchase
方法。實現過程比較複雜,但是每執行一步,就更新所有參數的狀態,按照我們之前整理出來的pTemp
用法,使用複製粘貼大法,從EPFile.init4load
和EPFile.load
中可以提取大量的重複代碼下來。
需要注意的是,load
中是餘額加上交易金額,purchase
中就應該是減去交易金額。因此我們必須手動實現decrease
方法。
實現方法和increase
類似,從後向前,一次取1byte,進行減法,同時更新借位,最後返回借位即可。
這裏有一個小插曲,在計算MAC2的時候,一般不會出什麼問題,但是在計算TAC的時候,就會出問題,這個坑在我最後debug的時候才發現(爲了找這個bug整整debug了一個多鍾= =)。於是我重新改變策略,使用在MAC2生成並且存進data中以後,將pTemp32新開一個數組(JCSystem.makeTransientByteArray),然後重新作爲中間變量進行操作,但是不知道爲什麼,這樣操作的結果還是錯誤的。於是就按照網上流傳的代碼,在計算TAC的時候,新申明瞭一個temp數組用來作爲臨時變量。
最終實現的代碼如圖。
參數變化的記錄如圖。
====更新,findKeyByType會傳入0x34作爲參數是因爲在《實驗2文檔》已經說明,0x34是TAC祕鑰的標識符。而我們本次操作就是爲了生成TAC,故而纔會使用0x34作爲參數。
最後一個就是get_balance
了。
直接從EP_balance
裏面讀出來就行了,一行代碼搞定。
到這裏,整個實驗中需要我們填寫的功能函數全部填寫完成了。試驗過程中,瞭解業務邏輯是一個很重要的過程,閱讀源碼,瞭解源碼中每一個函數的具體意義,以及每一個參數中,存放的信息有哪些,這些都最好都在一個txt或者什麼地方器記錄下來,方便自己查閱和提取(畢竟一個沒有debug的IDE,你不能要求什麼,只能人腦debug).另一方面,每實現一個功能,就儘可能寫清楚這個部分的功能,同時寫清楚那些信息在哪些位置,這樣你在實現的過程中才不會頭暈腦脹。也能夠方便你最後debug找錯誤信息從哪來。
驗證實驗
驗證之前,我們還需要添加新的參數信息。
以及一個的天坑的地方。。。。。。。這個地方我用throw 0x1234大法,從init_purchase的return 0之前,一直回退到Purse中的if(papdu.APDUContainData())才找到問題。
其他的地方應該沒什麼需要添加或者修改了,按照之前上一次的試驗方法,我們可以開始進行debug了。
首先還是新建一個txt,因爲我們需要人腦擔任終端機的角色,所以提前將必要的操作流程和模板寫下來存在txt裏面,寫指令的時候也能迅速一點。
首先實驗一中的初始化腳本。
然後模仿終端機輸入圈存初始化指令。/send 805000020B080000100000112233445510
使用[僞隨機數||電子錢包聯機交易序號||8000]作爲數據,圈存祕鑰作爲祕鑰,使用3DES獲得過程祕鑰。
然後使用過程祕藥作爲祕鑰,輸入自定義好的初始向量,以及數據[電子錢包餘額(交易前)||交易金額||交易類型標識(0x02)||終端機編號]作爲輸入,生成mac1,和IC卡返回的mac1校對,發現一致。
接着使用[電子錢包餘額(交易前)||交易金額||交易類型標識(0x02)||終端機編號]作爲數據,過程祕鑰作爲祕鑰,生成mac2。夾雜其他信息一起發出去。
返回TAC+9000
,所以我們認爲init_load
和load
成功。
接下來執行查詢指令,返回00 00 10 00 90 00
。說明卡里面已經有了00001000
,我們圈存確實成功了。查詢也成功了。
接下來執行消費指令,/send 805001020B07000010000011223344550F
返回得到僞隨機數。接着我們將[僞隨機數||電子錢包脫機交易序號||終端交易序號的最右兩個字節]作爲數據,消費祕鑰作爲祕鑰,使用3DES得到圈存祕鑰。
使用[交易金額||交易類型標識(0x06)||終端機編號||交易日期(主機)||交易時間(主機)]作爲數據,過程祕鑰最爲祕鑰生成mac1。
返回MAC2+TAC+9000
。故而我們認爲本次init_purchase
和purchase
成功。
爲了防止意外,我們再次執行查詢餘額的指令。
結果顯示餘額爲00000000
,返回9000
。故而我們認爲,查詢消費圈存三大功能都已經能夠正確執行了。
最後測試一下多次存取操作。沒什麼問題,故而我們認爲,本次實驗成功。
2017/4/25
更新,線下已校驗多次存取(複雜數據),以及TAC碼(我喜歡稱其工單碼)檢驗。經檢驗,不存在明顯問題。