# 9.5 存儲密碼
過去一段時間以來, 許多的網站遭遇用戶密碼數據泄露事件, 這其中包括頂級的互聯網企業–Linkedin, 國內諸如CSDN,該事件橫掃整個國內互聯網,隨后又爆出多玩游戲800萬用戶資料被泄露,另有傳言人人網、開心網、天涯社區、世紀佳緣、百合網等社區都有可能成為黑客下一個目標。層出不窮的類似事件給用戶的網上生活造成巨大的影響,人人自危,因為人們往往習慣在不同網站使用相同的密碼,所以一家“暴庫”,全部遭殃。
那么我們作為一個Web應用開發者,在選擇密碼存儲方案時, 容易掉入哪些陷阱, 以及如何避免這些陷阱?
## 普通方案
目前用的最多的密碼存儲方案是將明文密碼做單向哈希后存儲,單向哈希算法有一個特征:無法通過哈希后的摘要(digest)恢復原始數據,這也是“單向”二字的來源。常用的單向哈希算法包括SHA-256, SHA-1, MD5等。
Go語言對這三種加密算法的實現如下所示:
//import "crypto/sha256"
h := sha256.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))
//import "crypto/sha1"
h := sha1.New()
io.WriteString(h, "His money is twice tainted: 'taint yours and 'taint mine.")
fmt.Printf("% x", h.Sum(nil))
//import "crypto/md5"
h := md5.New()
io.WriteString(h, "需要加密的密碼")
fmt.Printf("%x", h.Sum(nil))
單向哈希有兩個特性:
- 1)同一個密碼進行單向哈希,得到的總是唯一確定的摘要。
- 2)計算速度快。隨著技術進步,一秒鐘能夠完成數十億次單向哈希計算。
結合上面兩個特點,考慮到多數人所使用的密碼為常見的組合,攻擊者可以將所有密碼的常見組合進行單向哈希,得到一個摘要組合, 然后與數據庫中的摘要進行比對即可獲得對應的密碼。這個摘要組合也被稱為`rainbow table`。
因此通過單向加密之后存儲的數據,和明文存儲沒有多大區別。因此,一旦網站的數據庫泄露,所有用戶的密碼本身就大白于天下。
## 進階方案
通過上面介紹我們知道黑客可以用`rainbow table`來破解哈希后的密碼,很大程度上是因為加密時使用的哈希算法是公開的。如果黑客不知道加密的哈希算法是什么,那他也就無從下手了。
一個直接的解決辦法是,自己設計一個哈希算法。然而,一個好的哈希算法是很難設計的——既要避免碰撞,又不能有明顯的規律,做到這兩點要比想象中的要困難很多。因此實際應用中更多的是利用已有的哈希算法進行多次哈希。
但是單純的多次哈希,依然阻擋不住黑客。兩次 MD5、三次 MD5之類的方法,我們能想到,黑客自然也能想到。特別是對于一些開源代碼,這樣哈希更是相當于直接把算法告訴了黑客。
沒有攻不破的盾,但也沒有折不斷的矛。現在安全性比較好的網站,都會用一種叫做“加鹽”的方式來存儲密碼,也就是常說的 “salt”。他們通常的做法是,先將用戶輸入的密碼進行一次MD5(或其它哈希算法)加密;將得到的 MD5 值前后加上一些只有管理員自己知道的隨機串,再進行一次MD5加密。這個隨機串中可以包括某些固定的串,也可以包括用戶名(用來保證每個用戶加密使用的密鑰都不一樣)。
//import "crypto/md5"
//假設用戶名abc,密碼123456
h := md5.New()
io.WriteString(h, "需要加密的密碼")
//pwmd5等于e10adc3949ba59abbe56e057f20f883e
pwmd5 :=fmt.Sprintf("%x", h.Sum(nil))
//指定兩個 salt: salt1 = @#$% salt2 = ^&*()
salt1 := "@#$%"
salt2 := "^&*()"
//salt1+用戶名+salt2+MD5拼接
io.WriteString(h, salt1)
io.WriteString(h, "abc")
io.WriteString(h, salt2)
io.WriteString(h, pwmd5)
last :=fmt.Sprintf("%x", h.Sum(nil))
在兩個salt沒有泄露的情況下,黑客如果拿到的是最后這個加密串,就幾乎不可能推算出原始的密碼是什么了。
## 專家方案
上面的進階方案在幾年前也許是足夠安全的方案,因為攻擊者沒有足夠的資源建立這么多的`rainbow table`。 但是,時至今日,因為并行計算能力的提升,這種攻擊已經完全可行。
怎么解決這個問題呢?只要時間與資源允許,沒有破譯不了的密碼,所以方案是:故意增加密碼計算所需耗費的資源和時間,使得任何人都不可獲得足夠的資源建立所需的`rainbow table`。
這類方案有一個特點,算法中都有個因子,用于指明計算密碼摘要所需要的資源和時間,也就是計算強度。計算強度越大,攻擊者建立`rainbow table`越困難,以至于不可繼續。
這里推薦`scrypt`方案,scrypt是由著名的FreeBSD黑客Colin Percival為他的備份服務Tarsnap開發的。
目前Go語言里面支持的庫http://code.google.com/p/go/source/browse?repo=crypto#hg%2Fscrypt
dk := scrypt.Key([]byte("some password"), []byte(salt), 16384, 8, 1, 32)
通過上面的的方法可以獲取唯一的相應的密碼值,這是目前為止最難破解的。
## 總結
看到這里,如果你產生了危機感,那么就行動起來:
- 1)如果你是普通用戶,那么我們建議使用LastPass進行密碼存儲和生成,對不同的網站使用不同的密碼;
- 2)如果你是開發人員, 那么我們強烈建議你采用專家方案進行密碼存儲。
## links
* [目錄](<preface.md>)
* 上一節: [確保輸入過濾](<09.4.md>)
* 下一節: [加密和解密數據](<09.6.md>)
- 目錄
- Go環境配置
- Go安裝
- GOPATH 與工作空間
- Go 命令
- Go開發工具
- 小結
- Go語言基礎
- 你好,Go
- Go基礎
- 流程和函數
- struct
- 面向對象
- interface
- 并發
- 小結
- Web基礎
- web工作方式
- Go搭建一個簡單的web服務
- Go如何使得web工作
- Go的http包詳解
- 小結
- 表單
- 處理表單的輸入
- 驗證表單的輸入
- 預防跨站腳本
- 防止多次遞交表單
- 處理文件上傳
- 小結
- 訪問數據庫
- database/sql接口
- 使用MySQL數據庫
- 使用SQLite數據庫
- 使用PostgreSQL數據庫
- 使用beedb庫進行ORM開發
- NOSQL數據庫操作
- 小結
- session和數據存儲
- session和cookie
- Go如何使用session
- session存儲
- 預防session劫持
- 小結
- 文本文件處理
- XML處理
- JSON處理
- 正則處理
- 模板處理
- 文件操作
- 字符串處理
- 小結
- Web服務
- Socket編程
- WebSocket
- REST
- RPC
- 小結
- 安全與加密
- 預防CSRF攻擊
- 確保輸入過濾
- 避免XSS攻擊
- 避免SQL注入
- 存儲密碼
- 加密和解密數據
- 小結
- 國際化和本地化
- 設置默認地區
- 本地化資源
- 國際化站點
- 小結
- 錯誤處理,調試和測試
- 錯誤處理
- 使用GDB調試
- Go怎么寫測試用例
- 小結
- 部署與維護
- 應用日志
- 網站錯誤處理
- 應用部署
- 備份和恢復
- 小結
- 如何設計一個Web框架
- 項目規劃
- 自定義路由器設計
- controller設計
- 日志和配置設計
- 實現博客的增刪改
- 小結
- 擴展Web框架
- 靜態文件支持
- Session支持
- 表單支持
- 用戶認證
- 多語言支持
- pprof支持
- 小結
- 參考資料