為了改進Android的安全問題蜂大,Google在Android系統(tǒng)中引入了谷歌驗證應用(Google Authenticator)來保證賬號的安全徘禁。谷歌驗證應用的使用方法是:用戶安裝手機客戶端勺拣,生成臨時身份驗證碼黔宛,提交到服務器驗證身份,類似的驗證系統(tǒng)還有Authy雳殊。Robbie在其GitHub頁面發(fā)布了自己用Go語言實現(xiàn)的版本哨颂,并撰寫了一篇博文來解釋其工作原理。
看過Robbie的代碼相种,你會發(fā)現(xiàn)他用到了系統(tǒng)的base32威恼,但是這里是有問題的,后文會說到
通常來講寝并,身份驗證系統(tǒng)都實現(xiàn)了基于時間的一次性密碼算法箫措,即著名的TOTP(Time-Based One-Time Password)。該算法由三部分組成:
- 一個共享密鑰(一系列二進制數(shù)據(jù))
- 一個基于當前時間的輸入
- 一個簽名函數(shù)
1. 共享密鑰
用戶在創(chuàng)建手機端身份驗證系統(tǒng)時需要獲取共享密鑰衬潦。獲取的方式包括用識別程序掃描給定二維碼或者直接手動輸入斤蔓。密鑰是三十二位加密,至于為什么不是六十四位镀岛,可以參考維基百科給出的解釋弦牡。
對于那些手動輸入的用戶,谷歌身份驗證系統(tǒng)給出的共享密鑰有如下的格式:
xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
256位數(shù)據(jù)漂羊,當然別的驗證系統(tǒng)可能會更短驾锰。
而對于掃描的用戶,QR識別以后是類似下面的URL鏈接:
otpauth://totp/xxx@xxx.com?secret=xxxx&issuer=Google
2. 基于當前時間的輸入
這個輸入是基于用戶手機時間產生的走越,一旦用戶完成第一步的密鑰共享椭豫,就和身份驗證服務器沒有關系了。但是這里比較重要的是用戶手機時間要準確,因為從算法原理來講赏酥,身份驗證服務器會基于同樣的時間來重復進行用戶手機的運算喳整。進一步來說,服務器會計算當前時間前后幾分鐘內的令牌裸扶,跟用戶提交的令牌比較框都。所以如果時間上相差太多,身份驗證過程就會失敗呵晨。
3. 簽名函數(shù)
谷歌的簽名函數(shù)使用了HMAC-SHA1瞬项。HMAC即基于哈希的消息驗證碼,提供了一種算法何荚,可以用比較安全的單向哈希函數(shù)(如SHA1)來產生簽名囱淋。這就是驗證算法的原理所在:只有共享密鑰擁有者和服務器才能夠根據(jù)同樣的輸入(基于時間的)得到同樣的輸出簽名。偽代碼如下:
hmac = SHA1(secret + SHA1(secret + input))
本文開頭提到的TOTP和HMAC原理類似餐塘,只是TOTP強調輸入一定是當前時間相關妥衣。類似的還有HOTP,采用增量式計數(shù)器的方式戒傻,需要不斷和服務器同步税手。
算法流程簡介
首先需要用base32解碼密鑰,為了更方便用戶輸入需纳,谷歌采用了空格和小寫的方式表示密鑰芦倒。但是base32不能有空格而且必須大寫,處理偽代碼如下:
original_secret = xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
secret = BASE32_DECODE(TO_UPPERCASE(REMOVE_SPACES(original_secret)))
golang中提供的
encoding/base32
庫解碼出來的secret
無論如何都不能得到跟Google Authenticator APP相同的結果不翩,最后發(fā)現(xiàn)兵扬,是golang的base32解碼出來的secret總是缺少位數(shù)】隍穑看了下源碼器钟,發(fā)現(xiàn)它的位數(shù)計算是:
func (enc *Encoding) DecodedLen(n int) int { return n / 8 * 5 }
這就意味著golangencoding/base32
解碼出的位數(shù)總是5的整數(shù)倍,why妙蔗?要這么處理傲霸!
接下來要從當前時間獲得輸入,通常采用Unix時間眉反,即當前周期開始到現(xiàn)在的秒數(shù)
input = CURRENT_UNIX_TIME()
這里有一點需要說明昙啄,驗證碼有一個時效,大概是30秒寸五。這種設計是出于方便用戶輸入的考慮梳凛,每秒鐘變化的驗證碼很難讓用戶迅速準確輸入。為了實現(xiàn)這種時效性播歼,可以通過整除30的方式來實現(xiàn)伶跷,即:
input = CURRENT_UNIX_TIME() / 30
最后一步是簽名函數(shù)掰读,HMAC-SHA1秘狞,全部偽代碼如下:
original_secret = xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
secret = BASE32_DECODE(TO_UPPERCASE(REMOVE_SPACES(original_secret)))
input = CURRENT_UNIX_TIME() / 30
hmac = SHA1(secret + SHA1(secret + input))
注意叭莫,SHA1中的input轉換為byte[8]的時候一定要是大端轉換
完成這些代碼,基本就已經實現(xiàn)了兩次驗證的功能烁试。由于HMAC是個標準長度的SHA1數(shù)值雇初,有四十個字符的長度,對于用戶來說太長减响,所以google會根據(jù)規(guī)則截取6位數(shù)字靖诗。可參考下面的偽代碼:
four_bytes = hmac[LAST_BYTE(hmac):LAST_BYTE(hmac) + 4]
large_integer = INT(four_bytes)
small_integer = large_integer % power(10,6)
完整代碼可以參考我的golang實現(xiàn)