使用 bcrypt 函式生成密碼

imxfly發表於2020-04-30

在幾年前,相信很多和我一樣的開發者都是使用 MD5 函式對使用者的密碼等敏感內容進行雜湊化後儲存到資料庫中。即便是現在,還是很多開發者是這樣的做法。

但很多事實告訴我們,如今用 MD5 函式生成的值在基於 *彩虹表? *和強大的 GPU 數億次每秒的暴力破解下能較為輕鬆的破解。

所以對於需要儲存使用者密碼等敏感資訊的需求場景下,我們需要尋找另一種可靠安全的加密方式。

有關為什麼 MD5 已經不可靠的原因可以參考我的另一篇文章《十萬個為什麼:別用 MD5 加密密碼》。

當下推薦的方案是使用 bcrypt 函式生成密碼,並不是因為它絕對安全不可破解,而是破解的成本足夠高。

世上沒有絕對的安全,我們能做的就是提高破解的成本。

兩個關鍵因素

首先 bcrypt 是一個密碼加密函式,由 Niels Provos 和 David Mazieres 設計,在 1999 年正式向世人提出。

這個函式由兩個關鍵因素確保其可靠安全:

第一點:就像很多其他加密函式或者方案一樣,會參雜一個 salt 進去,也就是我們常說的「加鹽」,有了鹽?,攻擊者通過彩虹表就無法破解了,他必須把鹽值也猜出來才有可能破解。

但是光是加鹽,很多加密函式或者我們使用 MD5 搭配鹽也能弄,不是使用它的強有力理由。

第二點:bcrypt 通過接受一個引數 cost 提高計算時長,換句話說,cost 數值越大,bcrypt 執行計算所需的時間就越長。

想象一個場景:

我們通過 MD5 + salt 的方案生成密碼,攻擊者拿到這個密碼後,基於彩虹表攻擊無效後,乾脆直接採用暴力搜尋攻擊,因為執行一次 MD5 函式所需的時間在強大的計算能力面前是很短暫的,所以也不需要花多少時間就能破解出來,我們假定執行一次 MD5 函式所需時間是 1 毫秒。

現在我們換成使用 bcrypt 函式生成密碼,我們生成的時候先指定這個 cost 引數值為 1,並且此時執行一次 bcrypt 函式所需時間也是 1 毫秒,但如果我們增大這個 cost 引數值,比如為 10,此時執行一次 bcrypt 函式所需時間可能是 50 毫秒,那麼等於是原先平均只需要 1 小時就能破解一個密碼現在需要 50 小時才能破解一個。

攻擊者往往是一次破解一批使用者的密碼,所以可以想象這個時間成本和算力成本有多大了。

如何選取合適的 cost

一般的,我們預設取 10 作為 cost 引數的值,比如 Go 中的 bcrypt.DefaultCost 就是 10。

我們也可以根據當前伺服器的效能選擇一個合適的 cost 值,比如我想執行一次 bcrypt 的時間不超過 200 毫秒,這樣既不會容易被破解,也不會太耗時,那麼我們可以根據下面這段 Go 程式碼來選擇出一個合適的 cost 值:

package main

import (
    "fmt"
    "time"

    "golang.org/x/crypto/bcrypt"
)

func main() {
    for cost := 10; cost <= 20; cost++ {
        start := time.Now()
        bcrypt.GenerateFromPassword([]byte("pa55w0rd"), cost)
        fmt.Printf("cost: %d, duration: %v\n", cost, time.Since(start))
    }
}

在我的本機上執行結果如下:

cost: 10, duration: 75.797197ms
cost: 11, duration: 146.597944ms
cost: 12, duration: 298.971358ms
cost: 13, duration: 610.758023ms
cost: 14, duration: 1.181615153s
cost: 15, duration: 2.433344989s
cost: 16, duration: 4.917117451s
cost: 17, duration: 9.453614867s
cost: 18, duration: 19.186913882s
cost: 19, duration: 37.79228015s
cost: 20, duration: 1m16.157706237s

那麼我就可以據此選擇 cost 值為 11,其實從上面的執行結果也能看出來,cost 值和執行時間之間的關係:cost 每增加一,執行耗時就會翻一倍。對具體演算法有興趣的人可以在文末的 Wikipedia 參考連結中找到更多相關資訊。

也附上以下 PHP 版本的吧:

<?php
for ($cost = 10; $cost <= 15; $cost++) {
    $start = microtime(true);
    password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
    $end = microtime(true);
    echo "cost: " . $cost . ", duration: " . ($end - $start) * 1000 . "\n";
}
?>

示例程式碼

這裡提供 PHP 和 Go 的兩段示例程式碼供參考:

<?php
$pwd = "pa55w0rd";
echo "Origin Password: " . $pwd . "\n";

$hash = password_hash($pwd, PASSWORD_BCRYPT, ["cost" => 10]);
echo "Encrypted Password: " . $hash . "\n";

$match = password_verify($pwd, $hash);
echo "Match Result: " . $match;
?>
package main

import (
    "fmt"

    "golang.org/x/crypto/bcrypt"
)

func main() {
    pwd := "pa55w0rd"

    fmt.Println("Origin Password: " + pwd)

    hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)

    fmt.Println("Encrypted Password: " + string(hash))

    err := bcrypt.CompareHashAndPassword(hash, []byte(pwd))
    fmt.Println("Match Result: ", err == nil)
}

參考連結

本作品採用《CC 協議》,轉載必須註明作者和本文連結

程式設計,在創造中體會快樂。

相關文章