您的位置:首頁>科技>正文

如何安全的存儲使用者密碼?

作者|Defuse Security團隊

編輯|Alice Qin

本文介紹了對密碼雜湊加密的基礎知識, 以及什麼是正確的加密方式。 還介紹了常見的密碼破解方法, 給出了如何避免密碼被破解的思路。 相信讀者閱讀本文後, 就會對密碼的加密有一個正確的認識, 並對密碼正確進行加密措施。

本文由Defuse Security安全團隊撰寫, 作者採用了 CC協議 , InfoQ翻譯並分享, 以饗讀者。

作為一名Web開發人員, 我們經常需要與使用者的帳號系統打交道, 而這其中最大的挑戰就是如何保護使用者的密碼。 經常會看到使用者帳戶資料庫頻繁被黑, 所以我們必須採取一些措施來保護使用者密碼,

以免導致不必要的資料洩露。 保護密碼的最好辦法是使用加鹽密碼雜湊( salted password hashing)。

在對密碼進行雜湊加密的問題上, 人們有很多爭論和誤解, 可能是由於網路上有大量錯誤資訊的原因吧。 對密碼雜湊加密是一件很簡單的事, 但很多人都犯了錯。 本文將會重點分享如何進行正確加密使用者密碼。

重要警告:請放棄編寫自己的密碼雜湊加密代碼的念頭!因為這件事太容易搞砸了。 就算你在大學學過密碼學的知識, 也應該遵循這個警告。 所有人都要謹記這點:不要自己寫雜湊加密演算法! 存儲密碼的相關問題已經有了成熟的解決方案, 就是使用 phpass, 或者在 defuse/password-hashing 或 libsodium 上的 PHP 、 C# 、 Java 和 Ruby 的實現。

密碼雜湊是什麼?

雜湊演算法是一種單向函數。

它把任意數量的資料轉換為固定長度的“指紋”, 而且這個過程無法逆轉。 它們有這樣的特性:如果輸入發生了一點改變, 由此產生的雜湊值會完全不同(參見上面的例子)。 這個特性很適合用來存儲密碼。 因為我們需要一種不可逆的演算法來加密存儲的密碼, 同時保證我們也能夠驗證使用者登陸的密碼是否正確。 在基於雜湊加密的帳號系統中, 使用者註冊和認證的大致流程如下。

用戶創建自己的帳號。

密碼經過雜湊加密後存儲在資料庫中。 密碼一旦寫入到磁片, 任何時候都不允許是明文形式。

當使用者試圖登錄時, 系統從資料庫取出已經加密的密碼, 和經過雜湊加密的使用者輸入的密碼進行對比。

如果雜湊值相同, 用戶將被授予存取權限。 否則, 告知用戶他們輸入的登陸憑據無效。

每當有人試圖嘗試登陸, 就重複步驟3和4。

在步驟4中, 永遠不要告訴用戶輸錯的究竟是用戶名還是密碼。 就像通用的提示那樣, 始終顯示:“無效的用戶名或密碼。 ”就行了。 這樣可以防止攻擊者在不知道密碼的情況下枚舉出有效的用戶名。

應當注意的是, 用來保護密碼的雜湊函數, 和資料結構課學到的雜湊函數是不同的。 例如, 實現雜湊表的雜湊函數設計目的是快速查找, 而非安全性。 只有加密雜湊函數( cryptographic hash function)才可以用來進行密碼雜湊加密。 像 SHA256 、 SHA512 、 RIPEMD 和 WHIRLPOOL 都是加密雜湊函數。

人們很容易認為, Web開發人員所做的就是:只需通過執行加密雜湊函數就可以讓使用者密碼得以安全。

然而並不是這樣。 有很多方法可以從簡單的雜湊值中快速恢復出明文的密碼。 有幾種易於實施的技術, 使這些“破解”的效率大為降低。 網上有這種專門破解MD5的網站, 只需提交一個雜湊值, 不到一秒鐘就能得到破解的結果。 顯然, 單純的對密碼進行雜湊加密遠遠達不到我們的安全要求。 下一節將討論一些用來破解簡單密碼雜湊常用的手段。

如何破解雜湊?

字典攻擊和暴力攻擊( Dictionary and Brute Force Attacks)

破解雜湊加密最簡單的方法是嘗試猜測密碼, 雜湊每個猜測的密碼, 並對比猜測密碼的雜湊值是否等於被破解的雜湊值。 如果相等, 則猜中。 猜測密碼攻擊的兩種最常見的方法是字典攻擊和暴力攻擊 。

字典攻擊使用包含單詞、短語、常用密碼和其他可能用做密碼的字串的字典檔。

對檔中的每個詞都進行雜湊加密, 將這些雜湊值和要破解的密碼雜湊值比較。 如果它們相同, 這個詞就是密碼。 字典檔是通過大段文本中提取的單詞構成, 甚至還包括一些資料庫中真實的密碼。 還可以對字典檔進一步處理以使其更為有效:如單詞 “hello” 按網路用語寫法轉成 “h3110” 。

暴力攻擊是對於給定的密碼長度, 嘗試每一種可能的字元組合。 這種方式會消耗大量的計算, 也是破解雜湊加密效率最低的辦法, 但最終會找出正確的密碼。 因此密碼應該足夠長, 以至於遍歷所有可能的字元組合, 耗費的時間太長令人無法承受, 從而放棄破解。

目前沒有辦法來組織字典攻擊或暴力攻擊。 只能想辦法讓它們變得低效。如果密碼雜湊系統設計是安全的,破解雜湊的唯一方法就是進行字典攻擊或暴力攻擊遍歷每一個雜湊值了。

查表法( Lookup Tables)

對於破解相同類型的雜湊值,查表法是一種非常高效的方式。主要理念是預先計算( pre-compute)出密碼字典中的每個密碼的雜湊值,然後把他們相應的密碼存儲到一個表裡。一個設計良好的查詢表結構,即使包含了數十億個雜湊值,仍然可以實現每秒鐘查詢數百次雜湊。

如果你想感受查表法的速度有多快,嘗試一下用 CrackStation 的 free hash cracker 來破解下面的 SHA256。

反向查表法( Reverse Lookup Tabs)

這種攻擊允許攻擊者無需預先計算好查詢表的情況下同時對多個雜湊值發起字典攻擊或暴力攻擊。

首先,攻擊者從被黑的使用者帳號資料庫創建一個用戶名和對應的密碼雜湊表,然後,攻擊者猜測一系列雜湊值並使用該查詢表來查找使用此密碼的使用者。通常許多使用者都會使用相同的密碼,因此這種攻擊方式特別有效。

彩虹表( Rainbow Tables)

彩虹表是一種以空間換時間的技術。與查表法相似,只是它為了使查詢表更小,犧牲了破解速度。因為彩虹表更小,所以在單位空間可以存儲更多的雜湊值,從而使攻擊更有效。能夠破解任何最多8位長度的 MD5 值的彩虹表已經出現。

接下來,我們來看一種謂之“加鹽( salting)”的技術,能夠讓查表法和彩虹表都失效。

加鹽( Adding Salt)

查表法和彩虹表只有在所有密碼都以完全相同的方式進行雜湊加密才有效。如果兩個使用者有相同的密碼,他們將有相同的密碼雜湊值。我們可以通過“隨機化”雜湊,當同一個密碼雜湊兩次後,得到的雜湊值是不一樣的,從而避免了這種攻擊。

我們可以通過在密碼中加入一段隨機字串再進行雜湊加密,這個被加的字串稱之為鹽值。如上例所示,這使得相同的密碼每次都被加密為完全不同的字串。我們需要鹽值來校驗密碼是否正確。通常和密碼雜湊值一同存儲在帳號資料庫中,或者作為雜湊字串的一部分。

鹽值無需加密。由於隨機化了雜湊值,查表法、反向查表法和彩虹表都會失效。因為攻擊者無法事先知道鹽值,所以他們就沒有辦法預先計算查詢表或彩虹表。如果每個使用者的密碼用不同的鹽再進行雜湊加密,那麼反向查表法攻擊也將不能奏效。

接下來,我們看看加鹽雜湊通常會有哪些不正確的措施。

錯誤的方法:短鹽值和鹽值複用

最常見的錯誤,是多次雜湊加密使用相同的鹽值,或者鹽值太短。

鹽值複用( Salt Reuse)

一個常見的錯誤是每次都使用相同的鹽值進行雜湊加密,這個鹽值要麼被硬編碼到程式裡,要麼只在第一次使用時隨機獲得。這樣的做法是無效的,因為如果兩個使用者有相同的密碼,他們仍然會有相同的雜湊值。攻擊者仍然可以使用反向查表法對每個雜湊值進行字典攻擊。他們只是在雜湊密碼之前,將固定的鹽值應用到每個猜測的密碼就可以了。如果鹽值被硬編碼到一個流行的軟體裡,那麼查詢表和彩虹表可以內置該鹽值,以使其更容易破解它產生的雜湊值。

使用者創建帳號或者更改密碼時,都應該用新的隨機鹽值進行加密。

短鹽值( Short Slat)

如果鹽值太短,攻擊者可以預先製作針對所有可能的鹽值的查詢表。例如,如果鹽值只有三個 ASCII 字元,那麼只有 95x95x95=857,375種可能性。這看起來很多,但如果每個查詢表包含常見的密碼只有 1MB,857,375個鹽值總共只需 837GB,一塊時下不到100美元的 1TB硬碟就能解決問題了。

出於同樣的原因,不應該將用戶名用作鹽值。對每一個服務來說,用戶名是唯一的,但它們是可預測的,並且經常重複應用於其他服務。攻擊者可以用常見用戶名作為鹽值來建立查詢表和彩虹表來破解密碼雜湊。

為使攻擊者無法構造包含所有可能鹽值的查詢表,鹽值必須足夠長。一個好的經驗是使用和雜湊函數輸出的字串等長的鹽值。例如, SHA256 的輸出為256位元(32位元組),所以該鹽也應該是32個隨機位元組。

雙重雜湊和古怪的雜湊函數

本節將介紹另一種常見的密碼雜湊的誤解:古怪雜湊的演算法組合。人們很容易衝昏頭腦,嘗試不同的雜湊函數相結合一起使用,希望讓資料會更安全。但在實踐中,這樣做並沒有什麼好處。它帶來了函數之間互通性的問題,而且甚至可能會使雜湊變得更不安全。永遠不要試圖去創造你自己的雜湊加密演算法,要使用專家設計好的標準演算法。有人會說,使用多個雜湊函數會降低計算速度,從而增加破解的難度。但是使破解過程變慢還有更好的辦法,我們將在後面講到。

下面是在網上見過的古怪的雜湊函數組合的一些例子。

md5(sha1(password))

md5(md5(salt) + md5(password))

sha1(sha1(password))

sha1(str_rot13(password + salt))

md5(sha1(md5(md5(password) + sha1(password)) + md5(password))))

不要使用其中任何一種。

注意:此部分是有爭議的。我收到了一些電子郵件,他們認為古怪的雜湊函數是有意義的,理由是,如果攻擊者不知道系統使用哪個雜湊函數,那麼攻擊者就不太可能預先計算出這種古怪的雜湊函數彩虹表,於是破解起來要花更多的時間。

當攻擊者不知道雜湊加密演算法的時候,是無法發起攻擊的。但是要考慮到柯克霍夫原則,攻擊者通常會獲得原始程式碼(尤其是免費或者開源軟體)。通過系統中找出密碼-雜湊值對應關係,很容易反向推導出加密演算法。使用一個很難被平行計算結果的反覆運算演算法(下面將予以討論),然後增加適當的鹽值防止彩虹表攻擊。

如果你真的想用一個標準的“古怪”的雜湊函數,如 HMAC ,亦無不可。但是,如果你目的是想降低雜湊計算速度,那麼可以閱讀下面有關金鑰擴展的部分。

如果創造新的雜湊函數,可能會帶來風險,構造希函數的組合又會導致函數互通性的問題。它們帶來一點的好處和這些比起來微不足道。很顯然,最好的辦法是,使用標準、經過完整測試的演算法。

雜湊碰撞( Hash Collisions)

由於雜湊函數將任意大小的資料轉化為定長的字串,因此,必定有一些不同的輸入經過雜湊計算後得到了相同的字串的情況。加密雜湊函數( Cryptographic hash function)的設計初衷就是使這些碰撞儘量難以被找到。現在,密碼學家發現攻擊雜湊函數越來越容易找到碰撞了。最近的例子是MD5演算法,它的碰撞已經實現了。

碰撞攻擊是指存在一個和使用者密碼不同的字串,卻有相同的雜湊值。然而,即使是像MD5這樣的脆弱的雜湊函數找到碰撞也需要大量的專門算力( dedicated computing power),所以在實際中“意外地”出現雜湊碰撞的情況不太可能。對於實用性而言,加鹽 MD5 和加鹽 SHA256 的安全性一樣。儘管如此,可能的話,要使用更安全的雜湊函數,比如 SHA256 、 SHA512 、 RipeMD 或 WHIRLPOOL 。

如何正確進行雜湊加密

本節介紹了究竟應該如何對密碼進行雜湊加密。第一部分介紹基礎知識,這部分是必須的。後面闡述如何在這個基礎上增強安全性,使雜湊加密變得更難破解。

基礎知識:加鹽雜湊( Hashing with Salt)

我們已經知道,惡意攻擊者使用查詢表和彩虹表,破解普通雜湊加密有多麼快。我們也已經瞭解到,使用隨機加鹽雜湊可以解決這個問題。但是,我們使用什麼樣的鹽值,又如何將其混入密碼中?

鹽值應該使用加密的安全偽亂數產生器( Cryptographically Secure Pseudo-Random Number Generator,CSPRNG )產生。CSPRNG和普通的偽亂數產生器有很大不同,如“ C ”語言的rand函數。顧名思義, CSPRNG 被設計成用於加密安全,這意味著它能提供高度隨機、完全不可預測的亂數。我們不希望鹽值能夠被預測到,所以必須使用 CSPRNG 。下表列出了一些當前主流程式設計平臺的 CSPRNG 方法。

每個使用者的每一個密碼都要使用獨一無二的鹽值。使用者每次創建帳號或更改密碼時,密碼應採用一個新的隨機鹽值。永遠不要重複使用某個鹽值。這個鹽值也應該足夠長,以使有足夠多的鹽值能用於雜湊加密。一個經驗規則是,鹽值至少要跟雜湊函數的輸出一樣長。該鹽應和密碼雜湊一起存儲在使用者帳號表中。

存儲密碼的步驟:

使用 CSPRNG 生成足夠長的隨機鹽值。

將鹽值混入密碼,並使用標準的密碼雜湊函數進行加密,如Argon2、 bcrypt 、 scrypt 或 PBKDF2 。

將鹽值和對應的雜湊值一起存入使用者資料庫。

校驗密碼的步驟:

從資料庫檢索出使用者的鹽值和對應的雜湊值。

將鹽值混入使用者輸入的密碼,並且使用通用的雜湊函數進行加密。

比較上一步的結果,是否和資料庫存儲的雜湊值相同。如果它們相同,則表明密碼是正確的;否則,該密碼錯誤。

在 Web 應用中,永遠在服務端上進行雜湊加密

如果您正在編寫一個 Web 應用,你可能會疑惑究竟在哪裡進行雜湊加密,是在用戶的流覽器上使用 JavaScript 對密碼進行雜湊加密呢,還是將明文發送到服務端上再進行雜湊加密呢?

就算流覽器上已經用JavaScript雜湊加密了,但你你還是要在服務端上將得到的密碼雜湊值再進行一次雜湊加密。試想一個網站,將用戶在流覽器輸入的密碼經過雜湊加密,而不是在傳送到服務端再進行雜湊。為了驗證用戶,這個網站將接受來自流覽器的雜湊值,並和資料庫中的雜湊值進行匹配即可。因為使用者的密碼從未明文傳輸到服務端,這樣子看上去更安全,但事實並非如此。

問題是,從用戶端的角度來看,經過雜湊的密碼,從邏輯上成為使用者的密碼了。所有使用者需要做的認證就是將它們的密碼雜湊值告訴服務端。如果一個攻擊者得到了用戶的雜湊值,他們可以用它來通過認證,而不必知道用戶的純文字密碼!所以,如果攻擊者使用某種手段拖了網站的資料庫,他們就可以隨意使用每個人的帳號直接訪問,而無需猜測任何密碼。

這並不是說你不應該在流覽器進行雜湊加密,但是如果你這樣做了,你一定要在服務端上再進行一次雜湊加密。在流覽器中進行雜湊加密無疑是一個好主意,但實現的時候要考慮以下幾點:

用戶端密碼雜湊加密不是 HTTPS(SSL/TLS)的替代品。如果流覽器和服務端之間的連接是不安全的,那麼中間人攻擊可以修改 JavaScript 代碼,刪除加密函數,從而獲取使用者的密碼。

某些流覽器不支持 JavaScript ,還有一些用戶在流覽器中禁用 JavaScript 功能。因此,為了更好的相容性,您的應用應該檢測流覽器是否支援 JavaScript ,如果不支援,就需要在服務端類比用戶端進行雜湊加密。

用戶端的雜湊加密同樣需要加鹽。顯而易見的解決方案是使用戶端指令碼向服務端請求使用者的鹽值。但是不提倡這樣做,因為它可以讓攻擊者能夠在不知道密碼的情況下檢測用戶名是否有效。既然你已經在服務端上對密碼進行了加鹽雜湊(使用合格的鹽值),那麼在用戶端,將用戶名(或郵箱)加上網站特有的字串(如功能變數名稱)作為用戶端的鹽值也是可行的。

使密碼更難破解:慢雜湊函數( Slow Hash Function)

加鹽可以確保攻擊者無法使用像查詢表和彩虹表攻擊那樣對大量雜湊值進行破解,但依然不能阻止他們使用字典攻擊或暴力攻擊。高端顯卡( GPU )和定制的硬體每秒可以進行十億次雜湊計算,所以這些攻擊還是很有效的。為了降低使這些攻擊的效率,我們可以使用一個叫做金鑰擴展( key stretching)的技術。

這樣做的初衷是為了將雜湊函數變得非常慢,即使有一塊快速的 GPU 或定制的硬體,字典攻擊和暴力攻擊也會慢得令人失去耐心。終極目標是使雜湊函數的速度慢到足以令攻擊者放棄,但由此造成的延遲又不至於引起用戶的注意。

金鑰擴展的實現使用了一種 CPU 密集型雜湊函數( CPU-intensive hash function)。不要試圖去創造你自己的反覆運算雜湊加密函數。反覆運算不夠多的話,它可以被高效的硬體快速平行計算出來,就跟普通的雜湊一樣。要使用標準的演算法,比如 PBKDF2 或 bcrypt 。你可以在這裡找到 PBKDF2 在 PHP 上的實現。

這類演算法採取安全因數或反覆運算次數作為參數。此值決定雜湊函數將會如何緩慢。對於桌面軟體或智慧手機應用,確定這個參數的最佳方式是在設備上運行很短的性能基準測試,找到使雜湊大約花費半秒的值。通過這種方式,程式可以盡可能保證安全而又不影響用戶體驗。

如果您想在一個 Web 應用使用金鑰擴展,須知你需要額外的計算資源來處理大量的身份認證請求,並且金鑰擴展也容易讓服務端遭受拒絕服務攻擊( DoS )。儘管如此,我還是建議使用金鑰擴展,只不過要設定較低一些的反覆運算次數。這個次數需要根據自己伺服器的計算能力和預計每秒需要處理的認證請求次數來設置。消除拒絕服務的威脅可以通過要求用戶每次登陸時輸入驗證碼( CAPTCHA )來做到。系統設計時要將反覆運算次數可隨時方便調整。

如果你擔心計算帶來負擔,但又想在 Web 應用中使用金鑰擴展,可以考慮在流覽器中使用 JavaScript 完成。斯坦福大學的 JavaScript 加密庫就包含了 PBKDF2 的實現。反覆運算次數應設置足夠低,以適應速度較慢的用戶端,如移動設備。同時,如果用戶的流覽器不支援 JavaScript ,服務端應該接手進行計算。用戶端金鑰擴展並不能免除服務端端進行雜湊加密的需要。你必須對用戶端生成的雜湊值再次進行雜湊加密,就跟普通口令的處理一樣。

不可能破解的雜湊加密:金鑰雜湊和密碼雜湊設備

只要攻擊者可以使用雜湊來檢查密碼的猜測是對還是錯,那麼他們可以進行字典攻擊或暴力攻擊。下一步是將金鑰( secret key)添加到雜湊加密,這樣只有知道金鑰的人才可以驗證密碼。有兩種實現的方式,使用ASE演算法對雜湊值加密;或者使用金鑰雜湊演算法 HMAC 將金鑰包含到雜湊字串中。

實現起來並沒那麼容易。這個金鑰必須在任何情況下,即使系統因為漏洞被攻陷,也不能被攻擊者獲取。如果攻擊者完全進入系統,金鑰不管存儲在何處,總能被找到。因此,金鑰必須金鑰必須被存儲在外部系統,例如專用於密碼驗證一個物理上隔離的服務端,或者連接到服務端,例如一個特殊的硬體設備,如 YubiHSM 。

我強烈建議所有大型服務(超過10萬使用者)使用這種方式。我認為對於任何超過100萬使用者的服務託管是非常有必要的。

如果您難以負擔多個服務端或專用硬體的費用,依然有辦法在標準的Web服務端上使用金鑰雜湊技術。大多數資料庫被拖庫是由於 SQL 注入攻擊,因此,不要給攻擊者進入本地檔案系統的許可權(禁止資料庫服務訪問本地檔案系統,如果有此功能的話)。

如果您生成一個隨機金鑰並將其存儲在一個通過 Web 無法訪問的檔上,然後進行加鹽雜湊加密,那麼得到的雜湊值就不會那麼容易被破解了,就算資料庫已經遭受注入攻擊,也是安全的。不要將金鑰硬編碼到代碼中,應該在安裝應用時隨機生成。這麼做並不像使用一個獨立的系統那樣安全,因為如果 Web 應用存在 SQL 注入點,那麼有可能存在其他一些問題,如本地檔包含漏洞( Local File Inclusion ),攻擊者可以利用它讀取本地金鑰檔。無論如何,這個措施總比沒有好。

請注意,金鑰雜湊並不意味著無需進行加鹽。高明的攻擊者最終會想方設法找到金鑰,因此,對密碼雜湊仍然需要進行加鹽和金鑰擴展,這一點非常重要。

其他安全措施

密碼雜湊僅僅在安全受到破壞時保護密碼。它並不能使整個應用更加安全。首先有很多事必須完成,來保證密碼雜湊值(和其他使用者資料)不被竊取。

即使是經驗豐富的開發人員也必須學習安全知識,才能編寫安全的應用。此處有關於Web應用漏洞的重要資源: The Open Web Application Security Project (OWASP)。還有一個很好的介紹: OWASP Top Ten Vulnerability List 。除非你理解了列表中的所有漏洞,否則不要去嘗試編寫一個處理敏感性資料的Web應用程式。雇主也有責任確保所有開發人員在安全應用開發方面經過充分的培訓。

對您的應用進行協力廠商“滲透測試”是一個很好的主意。即使最好的程式師也可能會犯錯,所以,讓安全專家審計代碼尋找潛在的漏洞是有意義的。找一個值得信賴的機構(或招聘人員)來定期審計代碼。安全審計應該從開發初期就著手進行,並貫穿整個開發過程。

監控您的網站來發現入侵行為也很重要。我建議至少雇用一名全職人員負責監測和處理安全性漏洞。如果某個漏洞沒被發現,攻擊者可能通過網站利用惡意軟體感染訪問者,因此,檢測漏洞並及時處理是極為重要的。

常見疑問

我應該使用什麼樣的雜湊演算法?

可以使用:

精心設計的金鑰擴展演算法如 PBKDF2 、bcrypt 和 scrypt 。

OpenWall的的 Portable PHP password hashing framework。

PBKDF2在PHP、C#、Java和Ruby的實現。

crypt 的安全版本。

不可使用:

快速加密雜湊函數,如 MD5 、SHA1、SHA256、SHA512、RipeMD、WHIRLPOOL、SHA3等。

crypt的不安全版本。

任何自己設計的加密演算法。只應該使用那些在公開領域中的、由經驗豐富的密碼學家完整測試過的技術。

儘管目前還沒有一種針對MD5或SHA1非常高效的攻擊手段,但它們過於古老以至於被廣泛認為不足以用來存儲密碼(可能有些不恰當)。所以我不推薦使用它們。但是也有例外,PBKDF2中經常使用SHA1作為它底層的雜湊函數。

當使用者忘記密碼時如何重置密碼?

這是我個人的觀點:當下所有廣泛使用的密碼重置機制都是不安全的。如果你對高安全性有要求,如加密服務,那麼就不要讓使用者重設密碼。

大多數網站向那些忘記密碼的使用者發送電子郵件來進行身份認證。要做到這一點,需要隨機生成一個一次性使用的權杖( token ),直接關聯到用戶的帳號。然後將這個權杖混入一個重置密碼的連結中,發送到使用者的電子郵箱。當使用者點擊包含有效權杖的密碼重置連結,就提示他們輸入新密碼。確保權杖只對一個帳號有效,以防攻擊者從郵箱獲取到權杖後用來重置其他使用者的密碼。

權杖必須在15分鐘內使用,且一旦使用後就立即作廢。當用戶登錄成功時(表明還記得自己的密碼), 或者重新請求權杖時,使原權杖失效是一個好做法。如果權杖永不過期,那麼它就可以一直用於入侵用戶的帳號。電子郵件(SMTP)是一個純文字協定,網路上有很多惡意路由在截取郵件資訊。在使用者修改密碼後,那些包含重置密碼連結的郵件在很長時間內缺乏保護,因此,儘早使權杖儘快過期,來降低使用者資訊暴露給攻擊者的風險。

攻擊者能夠篡改權杖,因此不要把帳號資訊和失效時間存儲在其中。它們應該以不可猜測的二進位形式存在,並且只用來識別資料庫中某條使用者的記錄。

千萬不要通過電子郵件向使用者發送新密碼。記得在使用者重置密碼時隨機生成一個新的鹽值用來加密,不要重複使用已用於密碼雜湊加密的舊鹽值。

如果帳號資料庫被洩漏或入侵,應該怎麼做?

你的首要任務是,確定系統被暴露到什麼程度,然後修復攻擊者利用的的漏洞。如果你沒有應對入侵的經驗,我強烈建議聘請協力廠商安全公司來做這件事。

捂住一個漏洞並期待沒人知道,是不是很省事,又誘人?但是這樣做只會讓你的處境變得更糟糕,因為你在使用者不知情的情況下,將它們的密碼和個人資訊置於暴露風險之中。就算你還沒有完全發生什麼事情時,你也應該儘快通知用戶。例如在首頁放置一個連結,指向對此問題更為詳細的說明;如果可能的話通過電子郵件發送通知給每個使用者告知目前的情況。

向使用者說明他們的密碼究竟是如何被保護的:最好是使用了加鹽雜湊。但是,即使用了加鹽雜湊,惡意駭客仍然可以使用字典攻擊和暴力攻擊。如果使用者在很多服務使用相同的密碼,惡意駭客會利用他們找到的密碼去嘗試登陸其他網站。告知用戶這個風險,建議他們修改所有類似的密碼,不論密碼用在哪個服務上。強制他們下次登錄你的網站時更改密碼。大多數使用者會嘗試“修改”自己的密碼為原始密碼,以便記憶。您應該使用當前密碼雜湊值以確保使用者無法做到這一點。

就算有加鹽雜湊的保護,也存在攻擊者快速破解其中一些弱口令密碼的可能性。為了減少攻擊者使用這些密碼的機會,應該對這些密碼的帳號發送認證電子郵件,直到使用者修改了密碼。可參考前面提到的問題:當使用者忘記密碼時如何重置密碼?這其中有一些實現電子郵件認證的要點。

另外告訴你的使用者,網站存儲了哪些個人資訊。如果您的資料庫包括信用卡號碼,您應該通知用戶仔細檢查近期帳單並銷掉這張信用卡。

應該使用什麼樣的密碼策略?是否應該使用強式密碼?

如果您的服務沒有嚴格的安全要求,那麼不要對用戶進行限制。我建議在使用者輸入密碼時,頁面顯示出密碼強度,由他們自己決定需要多安全的密碼。如果你有特殊的安全需求,那就應該實施長度至少為12個字元的密碼,並且至少需要兩個字母、兩個數位和兩個符號。

不要過於頻繁地強制你的使用者更改密碼,最多每半年一次,超過這個次數,用戶就會感到疲勞。相反,更好的做法是教育使用者,當他們感覺密碼可能洩露時主動修改,並且提示使用者不要把密碼告訴任何人。如果這是一個商業環境,鼓勵員工利用工作時間熟記並使用他們的密碼。

如果攻擊者入侵了資料庫,他不能直接替換雜湊值登陸任意帳號麼?

是的,但如果有人入侵您的資料庫,他們很可能已經能夠訪問您的服務端上的所有內容,這樣他們就不需要登錄到您的帳號,就可以獲得他們想要的東西。密碼雜湊(對網站而言)的目的不是為了保護被入侵的網站,而是在入侵已經發生時保護資料庫中的密碼。

你可以通過給資料庫連接設置兩種許可權,防止密碼雜湊在遭遇注入攻擊時被篡改。一種許可權用於創建用戶,一種許可權用於用戶登陸。“創建用戶”的代碼應該能夠讀寫用戶表;但“用戶登陸”的代碼應該只能夠讀取用戶表而不能寫入。

為什麼要使用一種像HMAC的特殊演算法,而不是只將金鑰混入密碼?

如 MD5、SHA1、SHA2 和 Hash 函數使用 Merkle–Damgård ,這使得它們很容易受到所謂的長度擴展攻擊( length extension attack)。意思是給定的雜湊值 H(X),對於任意的字串 Y,攻擊者可以計算出 H(pad(X)+Y) 的值,而無需知道 X 的值。其中, pad(X) 是雜湊函數的填充函數。

這意味著,攻擊者不知道金鑰的情況下,仍然可以根據給定的雜湊值 H(key+message) 計算出 H(pad(key+message)+extension) 。如果該雜湊值用於身份認證,並依靠其中的金鑰來防止攻擊者篡改消息,這方法已經行不通。因為攻擊者無需知道金鑰也能構造出包含 message+extension 的一個有效的雜湊值。

目前尚不清楚攻擊者如何利用這種攻擊來快速破解密碼雜湊。然而,由於這種攻擊的出現,不建議使用普通的雜湊函數對金鑰進行雜湊加密。將來也許某個高明的密碼學家有一天發現利用長度擴展攻擊的新思路,從而更快的破解密碼,所以還是使用 HMAC 為好。

鹽值應該加到密碼之前還是之後?

無所謂,選擇一個並保持風格一致即可,以免出現交互操作方面的問題。鹽值加到密碼之前較為普遍。

為何本文的雜湊代碼都以固定時間比較雜湊值?

使用固定的時間來比較雜湊值可以防止攻擊者在線上系統使用基於時間差的攻擊,以此獲取密碼的雜湊值,然後進行本地破解。

比較兩個位元組序列(字串)是否相同的標準做法是,從第一個位元組開始,每個位元組逐一順序比較。只要發現某個位元組不同,就可以知道它們是不同的,立即返回false。如果遍歷整個字串沒有找到不同的位元組,可以確認兩個字串就是相同的,可以返回true。這意味著比較兩個字串,如果它們相同的長度不一樣,花費的時間不一樣。開始部分相同的長度越長,花費的時間也就越長。

例如,字串 “XYZABC” 和 “abcxyz” 的標準比較,會立即看到,第一個字元是不同的,就不需要檢查字串的其餘部分。相反,當字串 “aaaaaaaaaaB” 和 “aaaaaaaaaaZ” 進行比較時,比較演算法就需要遍歷最後一位元前所有的 “a” ,然後才能知道他們是不同的。

假設攻擊者試圖入侵一個線上系統,這個系統限制了每秒只能嘗試一次用戶認證。還假設攻擊者已經知道密碼雜湊所有的參數(鹽值、雜湊函數的類型等),除了密碼的雜湊值和密碼本身。如果攻擊者能精確測量線上系統耗時多久去比較他猜測的密碼和真實密碼,那麼他就能使用時序攻擊獲取密碼的雜湊值,然後進行離線破解,從而繞過系統對認證頻率的限制。

首先攻擊者準備256個字串,它們的雜湊值的第一位元組包含了所有可能的情況。他將每個字串發送給線上系統嘗試登陸,並記錄系統回應所消耗的時間。耗時最長的字串就是第一位元組相匹配的。攻擊者知道第一位元組後,並可以用同樣的方式繼續猜測第二位元組、第三位元組等等。一旦攻擊者獲得足夠長的雜湊值片段,他就可以在自己的機器上來破解,不受線上系統的限制。

在網路上進行這種攻擊似乎不可能。然而,有人已經實現了,並已證明是實用的。這就是為什麼本文提到的代碼,它利用固定時間去比較字串,而不管有多大的字串。

“慢比較( slowequals)”函數如何工作?

前一個問題解釋了為什麼“慢比較”是必要的,現在來解釋代碼如何工作。

該代碼使用異或運算子“^”來比較兩個整數是否相等,而不是“==”運算子。下面解釋原因。當且僅當兩位相等時,異或的結果將是零。這是因為:0 XOR 0 = 0,1 XOR 1 = 0,0 XOR 1 = 1,1 XOR 0 = 1如果我們將其應用到整數中每一位元,當且僅當位元組兩個整數各位都相等,結果才是0。

所以,在代碼的第一行中,如果a.length等於b.length ,相同的話得到0,否者得到非零值。然後使用異或比較陣列中各位元組,並且將結果和diff求或。如果有任何一個位元組不相同,diff就會變成非零值。因為或運算沒有“置0”的功能,所以迴圈結束後diff是0的話只有一種可能,那就是迴圈前兩個陣列長度相等(a.length == b.length),並且陣列中每一個位元組都相同(每次異或的結果都非0)。

我們需要使用XOR,而不是“==”運算子比較整數的原因是,“==”通常是編譯成一個分支的語句。例如,C語言代碼中“ diff &= a == b”可能編譯以下x86彙編:

其中的分支導致代碼運行的時間不固定,決定於兩個整數相等的程度和CPU內部的跳轉預測機制(branch prediction)。

而C語言代碼“diff |= a ^ b”會被編譯為下面的樣子,它執行的時間和兩個變數是否相等無關。

為何要進行雜湊?

使用者在你的網站上輸入密碼,是因為他們相信你能保證密碼的安全。如果你的資料庫遭到駭客攻擊,而使用者的密碼又不受保護,那麼惡意駭客可以利用這些密碼嘗試登陸其他網站和服務(大多數使用者會在所有地方使用相同的密碼)。這不僅僅關乎你網站的安全,更關係到用戶的安全。你有責任負責用戶的安全。

今日薦課

課程名稱:《 如何 Lead 高效軟體研發團隊 》

主講老師:麥克周,某知名 IT 技術公司技術高管、IBM 開發者論壇專欄作者、InfoQ 特約作者。

上課內容:StuQ 邀請擁有十餘年技術一線管理經驗的麥克周老師,通過 3 周 Mini 直播課程形式,分享講師積累的較為成熟的方法論和踏坑填坑經驗,幫助學員成長為一名優秀的技術管理者,尤其在處理中小型團隊的管理工作時,能更加合理高效,有的放矢。前80人預售優惠中。

上課週期:3周

開班時間:5月5日晚 20:00

學習形式:QQ學習交流群+直播視頻授課

預售優惠價:799 元(原價 998 元,目前報名享受預售優惠)

使用優惠碼 預售優惠價再減 100 元等於 699 元 !

課程優惠碼:A0C4PLHLZW(僅可使用 10 次)

-掃描下方二維碼報名學習課程-

點擊「 閱讀原文 」立即報名學習。

只能想辦法讓它們變得低效。如果密碼雜湊系統設計是安全的,破解雜湊的唯一方法就是進行字典攻擊或暴力攻擊遍歷每一個雜湊值了。

查表法( Lookup Tables)

對於破解相同類型的雜湊值,查表法是一種非常高效的方式。主要理念是預先計算( pre-compute)出密碼字典中的每個密碼的雜湊值,然後把他們相應的密碼存儲到一個表裡。一個設計良好的查詢表結構,即使包含了數十億個雜湊值,仍然可以實現每秒鐘查詢數百次雜湊。

如果你想感受查表法的速度有多快,嘗試一下用 CrackStation 的 free hash cracker 來破解下面的 SHA256。

反向查表法( Reverse Lookup Tabs)

這種攻擊允許攻擊者無需預先計算好查詢表的情況下同時對多個雜湊值發起字典攻擊或暴力攻擊。

首先,攻擊者從被黑的使用者帳號資料庫創建一個用戶名和對應的密碼雜湊表,然後,攻擊者猜測一系列雜湊值並使用該查詢表來查找使用此密碼的使用者。通常許多使用者都會使用相同的密碼,因此這種攻擊方式特別有效。

彩虹表( Rainbow Tables)

彩虹表是一種以空間換時間的技術。與查表法相似,只是它為了使查詢表更小,犧牲了破解速度。因為彩虹表更小,所以在單位空間可以存儲更多的雜湊值,從而使攻擊更有效。能夠破解任何最多8位長度的 MD5 值的彩虹表已經出現。

接下來,我們來看一種謂之“加鹽( salting)”的技術,能夠讓查表法和彩虹表都失效。

加鹽( Adding Salt)

查表法和彩虹表只有在所有密碼都以完全相同的方式進行雜湊加密才有效。如果兩個使用者有相同的密碼,他們將有相同的密碼雜湊值。我們可以通過“隨機化”雜湊,當同一個密碼雜湊兩次後,得到的雜湊值是不一樣的,從而避免了這種攻擊。

我們可以通過在密碼中加入一段隨機字串再進行雜湊加密,這個被加的字串稱之為鹽值。如上例所示,這使得相同的密碼每次都被加密為完全不同的字串。我們需要鹽值來校驗密碼是否正確。通常和密碼雜湊值一同存儲在帳號資料庫中,或者作為雜湊字串的一部分。

鹽值無需加密。由於隨機化了雜湊值,查表法、反向查表法和彩虹表都會失效。因為攻擊者無法事先知道鹽值,所以他們就沒有辦法預先計算查詢表或彩虹表。如果每個使用者的密碼用不同的鹽再進行雜湊加密,那麼反向查表法攻擊也將不能奏效。

接下來,我們看看加鹽雜湊通常會有哪些不正確的措施。

錯誤的方法:短鹽值和鹽值複用

最常見的錯誤,是多次雜湊加密使用相同的鹽值,或者鹽值太短。

鹽值複用( Salt Reuse)

一個常見的錯誤是每次都使用相同的鹽值進行雜湊加密,這個鹽值要麼被硬編碼到程式裡,要麼只在第一次使用時隨機獲得。這樣的做法是無效的,因為如果兩個使用者有相同的密碼,他們仍然會有相同的雜湊值。攻擊者仍然可以使用反向查表法對每個雜湊值進行字典攻擊。他們只是在雜湊密碼之前,將固定的鹽值應用到每個猜測的密碼就可以了。如果鹽值被硬編碼到一個流行的軟體裡,那麼查詢表和彩虹表可以內置該鹽值,以使其更容易破解它產生的雜湊值。

使用者創建帳號或者更改密碼時,都應該用新的隨機鹽值進行加密。

短鹽值( Short Slat)

如果鹽值太短,攻擊者可以預先製作針對所有可能的鹽值的查詢表。例如,如果鹽值只有三個 ASCII 字元,那麼只有 95x95x95=857,375種可能性。這看起來很多,但如果每個查詢表包含常見的密碼只有 1MB,857,375個鹽值總共只需 837GB,一塊時下不到100美元的 1TB硬碟就能解決問題了。

出於同樣的原因,不應該將用戶名用作鹽值。對每一個服務來說,用戶名是唯一的,但它們是可預測的,並且經常重複應用於其他服務。攻擊者可以用常見用戶名作為鹽值來建立查詢表和彩虹表來破解密碼雜湊。

為使攻擊者無法構造包含所有可能鹽值的查詢表,鹽值必須足夠長。一個好的經驗是使用和雜湊函數輸出的字串等長的鹽值。例如, SHA256 的輸出為256位元(32位元組),所以該鹽也應該是32個隨機位元組。

雙重雜湊和古怪的雜湊函數

本節將介紹另一種常見的密碼雜湊的誤解:古怪雜湊的演算法組合。人們很容易衝昏頭腦,嘗試不同的雜湊函數相結合一起使用,希望讓資料會更安全。但在實踐中,這樣做並沒有什麼好處。它帶來了函數之間互通性的問題,而且甚至可能會使雜湊變得更不安全。永遠不要試圖去創造你自己的雜湊加密演算法,要使用專家設計好的標準演算法。有人會說,使用多個雜湊函數會降低計算速度,從而增加破解的難度。但是使破解過程變慢還有更好的辦法,我們將在後面講到。

下面是在網上見過的古怪的雜湊函數組合的一些例子。

md5(sha1(password))

md5(md5(salt) + md5(password))

sha1(sha1(password))

sha1(str_rot13(password + salt))

md5(sha1(md5(md5(password) + sha1(password)) + md5(password))))

不要使用其中任何一種。

注意:此部分是有爭議的。我收到了一些電子郵件,他們認為古怪的雜湊函數是有意義的,理由是,如果攻擊者不知道系統使用哪個雜湊函數,那麼攻擊者就不太可能預先計算出這種古怪的雜湊函數彩虹表,於是破解起來要花更多的時間。

當攻擊者不知道雜湊加密演算法的時候,是無法發起攻擊的。但是要考慮到柯克霍夫原則,攻擊者通常會獲得原始程式碼(尤其是免費或者開源軟體)。通過系統中找出密碼-雜湊值對應關係,很容易反向推導出加密演算法。使用一個很難被平行計算結果的反覆運算演算法(下面將予以討論),然後增加適當的鹽值防止彩虹表攻擊。

如果你真的想用一個標準的“古怪”的雜湊函數,如 HMAC ,亦無不可。但是,如果你目的是想降低雜湊計算速度,那麼可以閱讀下面有關金鑰擴展的部分。

如果創造新的雜湊函數,可能會帶來風險,構造希函數的組合又會導致函數互通性的問題。它們帶來一點的好處和這些比起來微不足道。很顯然,最好的辦法是,使用標準、經過完整測試的演算法。

雜湊碰撞( Hash Collisions)

由於雜湊函數將任意大小的資料轉化為定長的字串,因此,必定有一些不同的輸入經過雜湊計算後得到了相同的字串的情況。加密雜湊函數( Cryptographic hash function)的設計初衷就是使這些碰撞儘量難以被找到。現在,密碼學家發現攻擊雜湊函數越來越容易找到碰撞了。最近的例子是MD5演算法,它的碰撞已經實現了。

碰撞攻擊是指存在一個和使用者密碼不同的字串,卻有相同的雜湊值。然而,即使是像MD5這樣的脆弱的雜湊函數找到碰撞也需要大量的專門算力( dedicated computing power),所以在實際中“意外地”出現雜湊碰撞的情況不太可能。對於實用性而言,加鹽 MD5 和加鹽 SHA256 的安全性一樣。儘管如此,可能的話,要使用更安全的雜湊函數,比如 SHA256 、 SHA512 、 RipeMD 或 WHIRLPOOL 。

如何正確進行雜湊加密

本節介紹了究竟應該如何對密碼進行雜湊加密。第一部分介紹基礎知識,這部分是必須的。後面闡述如何在這個基礎上增強安全性,使雜湊加密變得更難破解。

基礎知識:加鹽雜湊( Hashing with Salt)

我們已經知道,惡意攻擊者使用查詢表和彩虹表,破解普通雜湊加密有多麼快。我們也已經瞭解到,使用隨機加鹽雜湊可以解決這個問題。但是,我們使用什麼樣的鹽值,又如何將其混入密碼中?

鹽值應該使用加密的安全偽亂數產生器( Cryptographically Secure Pseudo-Random Number Generator,CSPRNG )產生。CSPRNG和普通的偽亂數產生器有很大不同,如“ C ”語言的rand函數。顧名思義, CSPRNG 被設計成用於加密安全,這意味著它能提供高度隨機、完全不可預測的亂數。我們不希望鹽值能夠被預測到,所以必須使用 CSPRNG 。下表列出了一些當前主流程式設計平臺的 CSPRNG 方法。

每個使用者的每一個密碼都要使用獨一無二的鹽值。使用者每次創建帳號或更改密碼時,密碼應採用一個新的隨機鹽值。永遠不要重複使用某個鹽值。這個鹽值也應該足夠長,以使有足夠多的鹽值能用於雜湊加密。一個經驗規則是,鹽值至少要跟雜湊函數的輸出一樣長。該鹽應和密碼雜湊一起存儲在使用者帳號表中。

存儲密碼的步驟:

使用 CSPRNG 生成足夠長的隨機鹽值。

將鹽值混入密碼,並使用標準的密碼雜湊函數進行加密,如Argon2、 bcrypt 、 scrypt 或 PBKDF2 。

將鹽值和對應的雜湊值一起存入使用者資料庫。

校驗密碼的步驟:

從資料庫檢索出使用者的鹽值和對應的雜湊值。

將鹽值混入使用者輸入的密碼,並且使用通用的雜湊函數進行加密。

比較上一步的結果,是否和資料庫存儲的雜湊值相同。如果它們相同,則表明密碼是正確的;否則,該密碼錯誤。

在 Web 應用中,永遠在服務端上進行雜湊加密

如果您正在編寫一個 Web 應用,你可能會疑惑究竟在哪裡進行雜湊加密,是在用戶的流覽器上使用 JavaScript 對密碼進行雜湊加密呢,還是將明文發送到服務端上再進行雜湊加密呢?

就算流覽器上已經用JavaScript雜湊加密了,但你你還是要在服務端上將得到的密碼雜湊值再進行一次雜湊加密。試想一個網站,將用戶在流覽器輸入的密碼經過雜湊加密,而不是在傳送到服務端再進行雜湊。為了驗證用戶,這個網站將接受來自流覽器的雜湊值,並和資料庫中的雜湊值進行匹配即可。因為使用者的密碼從未明文傳輸到服務端,這樣子看上去更安全,但事實並非如此。

問題是,從用戶端的角度來看,經過雜湊的密碼,從邏輯上成為使用者的密碼了。所有使用者需要做的認證就是將它們的密碼雜湊值告訴服務端。如果一個攻擊者得到了用戶的雜湊值,他們可以用它來通過認證,而不必知道用戶的純文字密碼!所以,如果攻擊者使用某種手段拖了網站的資料庫,他們就可以隨意使用每個人的帳號直接訪問,而無需猜測任何密碼。

這並不是說你不應該在流覽器進行雜湊加密,但是如果你這樣做了,你一定要在服務端上再進行一次雜湊加密。在流覽器中進行雜湊加密無疑是一個好主意,但實現的時候要考慮以下幾點:

用戶端密碼雜湊加密不是 HTTPS(SSL/TLS)的替代品。如果流覽器和服務端之間的連接是不安全的,那麼中間人攻擊可以修改 JavaScript 代碼,刪除加密函數,從而獲取使用者的密碼。

某些流覽器不支持 JavaScript ,還有一些用戶在流覽器中禁用 JavaScript 功能。因此,為了更好的相容性,您的應用應該檢測流覽器是否支援 JavaScript ,如果不支援,就需要在服務端類比用戶端進行雜湊加密。

用戶端的雜湊加密同樣需要加鹽。顯而易見的解決方案是使用戶端指令碼向服務端請求使用者的鹽值。但是不提倡這樣做,因為它可以讓攻擊者能夠在不知道密碼的情況下檢測用戶名是否有效。既然你已經在服務端上對密碼進行了加鹽雜湊(使用合格的鹽值),那麼在用戶端,將用戶名(或郵箱)加上網站特有的字串(如功能變數名稱)作為用戶端的鹽值也是可行的。

使密碼更難破解:慢雜湊函數( Slow Hash Function)

加鹽可以確保攻擊者無法使用像查詢表和彩虹表攻擊那樣對大量雜湊值進行破解,但依然不能阻止他們使用字典攻擊或暴力攻擊。高端顯卡( GPU )和定制的硬體每秒可以進行十億次雜湊計算,所以這些攻擊還是很有效的。為了降低使這些攻擊的效率,我們可以使用一個叫做金鑰擴展( key stretching)的技術。

這樣做的初衷是為了將雜湊函數變得非常慢,即使有一塊快速的 GPU 或定制的硬體,字典攻擊和暴力攻擊也會慢得令人失去耐心。終極目標是使雜湊函數的速度慢到足以令攻擊者放棄,但由此造成的延遲又不至於引起用戶的注意。

金鑰擴展的實現使用了一種 CPU 密集型雜湊函數( CPU-intensive hash function)。不要試圖去創造你自己的反覆運算雜湊加密函數。反覆運算不夠多的話,它可以被高效的硬體快速平行計算出來,就跟普通的雜湊一樣。要使用標準的演算法,比如 PBKDF2 或 bcrypt 。你可以在這裡找到 PBKDF2 在 PHP 上的實現。

這類演算法採取安全因數或反覆運算次數作為參數。此值決定雜湊函數將會如何緩慢。對於桌面軟體或智慧手機應用,確定這個參數的最佳方式是在設備上運行很短的性能基準測試,找到使雜湊大約花費半秒的值。通過這種方式,程式可以盡可能保證安全而又不影響用戶體驗。

如果您想在一個 Web 應用使用金鑰擴展,須知你需要額外的計算資源來處理大量的身份認證請求,並且金鑰擴展也容易讓服務端遭受拒絕服務攻擊( DoS )。儘管如此,我還是建議使用金鑰擴展,只不過要設定較低一些的反覆運算次數。這個次數需要根據自己伺服器的計算能力和預計每秒需要處理的認證請求次數來設置。消除拒絕服務的威脅可以通過要求用戶每次登陸時輸入驗證碼( CAPTCHA )來做到。系統設計時要將反覆運算次數可隨時方便調整。

如果你擔心計算帶來負擔,但又想在 Web 應用中使用金鑰擴展,可以考慮在流覽器中使用 JavaScript 完成。斯坦福大學的 JavaScript 加密庫就包含了 PBKDF2 的實現。反覆運算次數應設置足夠低,以適應速度較慢的用戶端,如移動設備。同時,如果用戶的流覽器不支援 JavaScript ,服務端應該接手進行計算。用戶端金鑰擴展並不能免除服務端端進行雜湊加密的需要。你必須對用戶端生成的雜湊值再次進行雜湊加密,就跟普通口令的處理一樣。

不可能破解的雜湊加密:金鑰雜湊和密碼雜湊設備

只要攻擊者可以使用雜湊來檢查密碼的猜測是對還是錯,那麼他們可以進行字典攻擊或暴力攻擊。下一步是將金鑰( secret key)添加到雜湊加密,這樣只有知道金鑰的人才可以驗證密碼。有兩種實現的方式,使用ASE演算法對雜湊值加密;或者使用金鑰雜湊演算法 HMAC 將金鑰包含到雜湊字串中。

實現起來並沒那麼容易。這個金鑰必須在任何情況下,即使系統因為漏洞被攻陷,也不能被攻擊者獲取。如果攻擊者完全進入系統,金鑰不管存儲在何處,總能被找到。因此,金鑰必須金鑰必須被存儲在外部系統,例如專用於密碼驗證一個物理上隔離的服務端,或者連接到服務端,例如一個特殊的硬體設備,如 YubiHSM 。

我強烈建議所有大型服務(超過10萬使用者)使用這種方式。我認為對於任何超過100萬使用者的服務託管是非常有必要的。

如果您難以負擔多個服務端或專用硬體的費用,依然有辦法在標準的Web服務端上使用金鑰雜湊技術。大多數資料庫被拖庫是由於 SQL 注入攻擊,因此,不要給攻擊者進入本地檔案系統的許可權(禁止資料庫服務訪問本地檔案系統,如果有此功能的話)。

如果您生成一個隨機金鑰並將其存儲在一個通過 Web 無法訪問的檔上,然後進行加鹽雜湊加密,那麼得到的雜湊值就不會那麼容易被破解了,就算資料庫已經遭受注入攻擊,也是安全的。不要將金鑰硬編碼到代碼中,應該在安裝應用時隨機生成。這麼做並不像使用一個獨立的系統那樣安全,因為如果 Web 應用存在 SQL 注入點,那麼有可能存在其他一些問題,如本地檔包含漏洞( Local File Inclusion ),攻擊者可以利用它讀取本地金鑰檔。無論如何,這個措施總比沒有好。

請注意,金鑰雜湊並不意味著無需進行加鹽。高明的攻擊者最終會想方設法找到金鑰,因此,對密碼雜湊仍然需要進行加鹽和金鑰擴展,這一點非常重要。

其他安全措施

密碼雜湊僅僅在安全受到破壞時保護密碼。它並不能使整個應用更加安全。首先有很多事必須完成,來保證密碼雜湊值(和其他使用者資料)不被竊取。

即使是經驗豐富的開發人員也必須學習安全知識,才能編寫安全的應用。此處有關於Web應用漏洞的重要資源: The Open Web Application Security Project (OWASP)。還有一個很好的介紹: OWASP Top Ten Vulnerability List 。除非你理解了列表中的所有漏洞,否則不要去嘗試編寫一個處理敏感性資料的Web應用程式。雇主也有責任確保所有開發人員在安全應用開發方面經過充分的培訓。

對您的應用進行協力廠商“滲透測試”是一個很好的主意。即使最好的程式師也可能會犯錯,所以,讓安全專家審計代碼尋找潛在的漏洞是有意義的。找一個值得信賴的機構(或招聘人員)來定期審計代碼。安全審計應該從開發初期就著手進行,並貫穿整個開發過程。

監控您的網站來發現入侵行為也很重要。我建議至少雇用一名全職人員負責監測和處理安全性漏洞。如果某個漏洞沒被發現,攻擊者可能通過網站利用惡意軟體感染訪問者,因此,檢測漏洞並及時處理是極為重要的。

常見疑問

我應該使用什麼樣的雜湊演算法?

可以使用:

精心設計的金鑰擴展演算法如 PBKDF2 、bcrypt 和 scrypt 。

OpenWall的的 Portable PHP password hashing framework。

PBKDF2在PHP、C#、Java和Ruby的實現。

crypt 的安全版本。

不可使用:

快速加密雜湊函數,如 MD5 、SHA1、SHA256、SHA512、RipeMD、WHIRLPOOL、SHA3等。

crypt的不安全版本。

任何自己設計的加密演算法。只應該使用那些在公開領域中的、由經驗豐富的密碼學家完整測試過的技術。

儘管目前還沒有一種針對MD5或SHA1非常高效的攻擊手段,但它們過於古老以至於被廣泛認為不足以用來存儲密碼(可能有些不恰當)。所以我不推薦使用它們。但是也有例外,PBKDF2中經常使用SHA1作為它底層的雜湊函數。

當使用者忘記密碼時如何重置密碼?

這是我個人的觀點:當下所有廣泛使用的密碼重置機制都是不安全的。如果你對高安全性有要求,如加密服務,那麼就不要讓使用者重設密碼。

大多數網站向那些忘記密碼的使用者發送電子郵件來進行身份認證。要做到這一點,需要隨機生成一個一次性使用的權杖( token ),直接關聯到用戶的帳號。然後將這個權杖混入一個重置密碼的連結中,發送到使用者的電子郵箱。當使用者點擊包含有效權杖的密碼重置連結,就提示他們輸入新密碼。確保權杖只對一個帳號有效,以防攻擊者從郵箱獲取到權杖後用來重置其他使用者的密碼。

權杖必須在15分鐘內使用,且一旦使用後就立即作廢。當用戶登錄成功時(表明還記得自己的密碼), 或者重新請求權杖時,使原權杖失效是一個好做法。如果權杖永不過期,那麼它就可以一直用於入侵用戶的帳號。電子郵件(SMTP)是一個純文字協定,網路上有很多惡意路由在截取郵件資訊。在使用者修改密碼後,那些包含重置密碼連結的郵件在很長時間內缺乏保護,因此,儘早使權杖儘快過期,來降低使用者資訊暴露給攻擊者的風險。

攻擊者能夠篡改權杖,因此不要把帳號資訊和失效時間存儲在其中。它們應該以不可猜測的二進位形式存在,並且只用來識別資料庫中某條使用者的記錄。

千萬不要通過電子郵件向使用者發送新密碼。記得在使用者重置密碼時隨機生成一個新的鹽值用來加密,不要重複使用已用於密碼雜湊加密的舊鹽值。

如果帳號資料庫被洩漏或入侵,應該怎麼做?

你的首要任務是,確定系統被暴露到什麼程度,然後修復攻擊者利用的的漏洞。如果你沒有應對入侵的經驗,我強烈建議聘請協力廠商安全公司來做這件事。

捂住一個漏洞並期待沒人知道,是不是很省事,又誘人?但是這樣做只會讓你的處境變得更糟糕,因為你在使用者不知情的情況下,將它們的密碼和個人資訊置於暴露風險之中。就算你還沒有完全發生什麼事情時,你也應該儘快通知用戶。例如在首頁放置一個連結,指向對此問題更為詳細的說明;如果可能的話通過電子郵件發送通知給每個使用者告知目前的情況。

向使用者說明他們的密碼究竟是如何被保護的:最好是使用了加鹽雜湊。但是,即使用了加鹽雜湊,惡意駭客仍然可以使用字典攻擊和暴力攻擊。如果使用者在很多服務使用相同的密碼,惡意駭客會利用他們找到的密碼去嘗試登陸其他網站。告知用戶這個風險,建議他們修改所有類似的密碼,不論密碼用在哪個服務上。強制他們下次登錄你的網站時更改密碼。大多數使用者會嘗試“修改”自己的密碼為原始密碼,以便記憶。您應該使用當前密碼雜湊值以確保使用者無法做到這一點。

就算有加鹽雜湊的保護,也存在攻擊者快速破解其中一些弱口令密碼的可能性。為了減少攻擊者使用這些密碼的機會,應該對這些密碼的帳號發送認證電子郵件,直到使用者修改了密碼。可參考前面提到的問題:當使用者忘記密碼時如何重置密碼?這其中有一些實現電子郵件認證的要點。

另外告訴你的使用者,網站存儲了哪些個人資訊。如果您的資料庫包括信用卡號碼,您應該通知用戶仔細檢查近期帳單並銷掉這張信用卡。

應該使用什麼樣的密碼策略?是否應該使用強式密碼?

如果您的服務沒有嚴格的安全要求,那麼不要對用戶進行限制。我建議在使用者輸入密碼時,頁面顯示出密碼強度,由他們自己決定需要多安全的密碼。如果你有特殊的安全需求,那就應該實施長度至少為12個字元的密碼,並且至少需要兩個字母、兩個數位和兩個符號。

不要過於頻繁地強制你的使用者更改密碼,最多每半年一次,超過這個次數,用戶就會感到疲勞。相反,更好的做法是教育使用者,當他們感覺密碼可能洩露時主動修改,並且提示使用者不要把密碼告訴任何人。如果這是一個商業環境,鼓勵員工利用工作時間熟記並使用他們的密碼。

如果攻擊者入侵了資料庫,他不能直接替換雜湊值登陸任意帳號麼?

是的,但如果有人入侵您的資料庫,他們很可能已經能夠訪問您的服務端上的所有內容,這樣他們就不需要登錄到您的帳號,就可以獲得他們想要的東西。密碼雜湊(對網站而言)的目的不是為了保護被入侵的網站,而是在入侵已經發生時保護資料庫中的密碼。

你可以通過給資料庫連接設置兩種許可權,防止密碼雜湊在遭遇注入攻擊時被篡改。一種許可權用於創建用戶,一種許可權用於用戶登陸。“創建用戶”的代碼應該能夠讀寫用戶表;但“用戶登陸”的代碼應該只能夠讀取用戶表而不能寫入。

為什麼要使用一種像HMAC的特殊演算法,而不是只將金鑰混入密碼?

如 MD5、SHA1、SHA2 和 Hash 函數使用 Merkle–Damgård ,這使得它們很容易受到所謂的長度擴展攻擊( length extension attack)。意思是給定的雜湊值 H(X),對於任意的字串 Y,攻擊者可以計算出 H(pad(X)+Y) 的值,而無需知道 X 的值。其中, pad(X) 是雜湊函數的填充函數。

這意味著,攻擊者不知道金鑰的情況下,仍然可以根據給定的雜湊值 H(key+message) 計算出 H(pad(key+message)+extension) 。如果該雜湊值用於身份認證,並依靠其中的金鑰來防止攻擊者篡改消息,這方法已經行不通。因為攻擊者無需知道金鑰也能構造出包含 message+extension 的一個有效的雜湊值。

目前尚不清楚攻擊者如何利用這種攻擊來快速破解密碼雜湊。然而,由於這種攻擊的出現,不建議使用普通的雜湊函數對金鑰進行雜湊加密。將來也許某個高明的密碼學家有一天發現利用長度擴展攻擊的新思路,從而更快的破解密碼,所以還是使用 HMAC 為好。

鹽值應該加到密碼之前還是之後?

無所謂,選擇一個並保持風格一致即可,以免出現交互操作方面的問題。鹽值加到密碼之前較為普遍。

為何本文的雜湊代碼都以固定時間比較雜湊值?

使用固定的時間來比較雜湊值可以防止攻擊者在線上系統使用基於時間差的攻擊,以此獲取密碼的雜湊值,然後進行本地破解。

比較兩個位元組序列(字串)是否相同的標準做法是,從第一個位元組開始,每個位元組逐一順序比較。只要發現某個位元組不同,就可以知道它們是不同的,立即返回false。如果遍歷整個字串沒有找到不同的位元組,可以確認兩個字串就是相同的,可以返回true。這意味著比較兩個字串,如果它們相同的長度不一樣,花費的時間不一樣。開始部分相同的長度越長,花費的時間也就越長。

例如,字串 “XYZABC” 和 “abcxyz” 的標準比較,會立即看到,第一個字元是不同的,就不需要檢查字串的其餘部分。相反,當字串 “aaaaaaaaaaB” 和 “aaaaaaaaaaZ” 進行比較時,比較演算法就需要遍歷最後一位元前所有的 “a” ,然後才能知道他們是不同的。

假設攻擊者試圖入侵一個線上系統,這個系統限制了每秒只能嘗試一次用戶認證。還假設攻擊者已經知道密碼雜湊所有的參數(鹽值、雜湊函數的類型等),除了密碼的雜湊值和密碼本身。如果攻擊者能精確測量線上系統耗時多久去比較他猜測的密碼和真實密碼,那麼他就能使用時序攻擊獲取密碼的雜湊值,然後進行離線破解,從而繞過系統對認證頻率的限制。

首先攻擊者準備256個字串,它們的雜湊值的第一位元組包含了所有可能的情況。他將每個字串發送給線上系統嘗試登陸,並記錄系統回應所消耗的時間。耗時最長的字串就是第一位元組相匹配的。攻擊者知道第一位元組後,並可以用同樣的方式繼續猜測第二位元組、第三位元組等等。一旦攻擊者獲得足夠長的雜湊值片段,他就可以在自己的機器上來破解,不受線上系統的限制。

在網路上進行這種攻擊似乎不可能。然而,有人已經實現了,並已證明是實用的。這就是為什麼本文提到的代碼,它利用固定時間去比較字串,而不管有多大的字串。

“慢比較( slowequals)”函數如何工作?

前一個問題解釋了為什麼“慢比較”是必要的,現在來解釋代碼如何工作。

該代碼使用異或運算子“^”來比較兩個整數是否相等,而不是“==”運算子。下面解釋原因。當且僅當兩位相等時,異或的結果將是零。這是因為:0 XOR 0 = 0,1 XOR 1 = 0,0 XOR 1 = 1,1 XOR 0 = 1如果我們將其應用到整數中每一位元,當且僅當位元組兩個整數各位都相等,結果才是0。

所以,在代碼的第一行中,如果a.length等於b.length ,相同的話得到0,否者得到非零值。然後使用異或比較陣列中各位元組,並且將結果和diff求或。如果有任何一個位元組不相同,diff就會變成非零值。因為或運算沒有“置0”的功能,所以迴圈結束後diff是0的話只有一種可能,那就是迴圈前兩個陣列長度相等(a.length == b.length),並且陣列中每一個位元組都相同(每次異或的結果都非0)。

我們需要使用XOR,而不是“==”運算子比較整數的原因是,“==”通常是編譯成一個分支的語句。例如,C語言代碼中“ diff &= a == b”可能編譯以下x86彙編:

其中的分支導致代碼運行的時間不固定,決定於兩個整數相等的程度和CPU內部的跳轉預測機制(branch prediction)。

而C語言代碼“diff |= a ^ b”會被編譯為下面的樣子,它執行的時間和兩個變數是否相等無關。

為何要進行雜湊?

使用者在你的網站上輸入密碼,是因為他們相信你能保證密碼的安全。如果你的資料庫遭到駭客攻擊,而使用者的密碼又不受保護,那麼惡意駭客可以利用這些密碼嘗試登陸其他網站和服務(大多數使用者會在所有地方使用相同的密碼)。這不僅僅關乎你網站的安全,更關係到用戶的安全。你有責任負責用戶的安全。

今日薦課

課程名稱:《 如何 Lead 高效軟體研發團隊 》

主講老師:麥克周,某知名 IT 技術公司技術高管、IBM 開發者論壇專欄作者、InfoQ 特約作者。

上課內容:StuQ 邀請擁有十餘年技術一線管理經驗的麥克周老師,通過 3 周 Mini 直播課程形式,分享講師積累的較為成熟的方法論和踏坑填坑經驗,幫助學員成長為一名優秀的技術管理者,尤其在處理中小型團隊的管理工作時,能更加合理高效,有的放矢。前80人預售優惠中。

上課週期:3周

開班時間:5月5日晚 20:00

學習形式:QQ學習交流群+直播視頻授課

預售優惠價:799 元(原價 998 元,目前報名享受預售優惠)

使用優惠碼 預售優惠價再減 100 元等於 699 元 !

課程優惠碼:A0C4PLHLZW(僅可使用 10 次)

-掃描下方二維碼報名學習課程-

點擊「 閱讀原文 」立即報名學習。

Next Article
喜欢就按个赞吧!!!
点击关闭提示