PHP Conference Japan 2024

password_hash

(PHP 5 >= 5.5.0, PHP 7, PHP 8)

password_hash建立密碼雜湊值

描述

password_hash(#[\SensitiveParameter] string $password, string|int|null $algo, array $options = []): string

password_hash() 使用強大的單向雜湊演算法建立新的密碼雜湊值。

目前支援下列演算法

  • PASSWORD_DEFAULT - 使用 bcrypt 演算法(PHP 5.5.0 起的預設值)。請注意,此常數旨在隨著新的更強大的演算法加入 PHP 而隨時間變更。因此,使用此識別符號的結果長度可能會隨著時間而變更。因此,建議將結果儲存在資料庫欄位中,使其可以擴展到 60 個字元以上(255 個字元會是不錯的選擇)。
  • PASSWORD_BCRYPT - 使用 CRYPT_BLOWFISH 演算法建立雜湊值。這會產生使用 "$2y$" 識別符號的標準 crypt() 相容雜湊值。結果會永遠是 60 個字元的字串,失敗則為 false
  • PASSWORD_ARGON2I - 使用 Argon2i 雜湊演算法建立雜湊值。此演算法僅在 PHP 編譯時有 Argon2 支援時才可用。
  • PASSWORD_ARGON2ID - 使用 Argon2id 雜湊演算法建立雜湊值。此演算法僅在 PHP 編譯時有 Argon2 支援時才可用。

PASSWORD_BCRYPT 的支援選項

  • salt (string) - 手動提供在雜湊密碼時使用的鹽。請注意,這會覆寫並防止自動產生鹽。

    如果省略,password_hash() 會為每個雜湊的密碼產生一個隨機鹽。這是預期的操作模式。

    警告

    已不建議使用鹽選項。現在偏好直接使用預設產生的鹽。從 PHP 8.0.0 開始,會忽略明確給定的鹽。

  • cost (int) - 表示應使用的演算法成本。這些值的範例可以在 crypt() 頁面找到。

    如果省略,將會使用預設值 10。這是很好的基準成本,但您可能需要考慮根據您的硬體來提高它。

PASSWORD_ARGON2IPASSWORD_ARGON2ID 的支援選項

參數

password

使用者的密碼。

注意

使用 PASSWORD_BCRYPT 作為演算法,會導致 password 參數截斷為最大長度 72 個位元組。

algo

密碼演算法常數,表示雜湊密碼時使用的演算法。

options

包含選項的關聯陣列。請參閱 密碼演算法常數以取得每個演算法的支援選項文件。

如果省略,將會建立隨機鹽,並使用預設成本。

傳回值

傳回雜湊後的密碼。

使用的演算法、成本和鹽會做為雜湊值的一部分傳回。因此,驗證雜湊值所需的所有資訊都包含在其中。這允許 password_verify() 函式驗證雜湊值,而無需為鹽或演算法資訊單獨儲存。

變更日誌

版本 描述
8.3.0 如果由於鹽產生失敗而拋出 ValueErrorpassword_hash() 現在會將底層的 Random\RandomException 設定為 Exception::$previous 例外狀況。
8.0.0 如果密碼雜湊演算法無效,或密碼雜湊因不明錯誤而失敗,password_hash() 不再於失敗時傳回 false,而是會拋出 ValueError,若密碼雜湊失敗則會拋出 Error
8.0.0 algo 參數現在可為 null。
7.4.0 algo 參數現在預期為 string,但為了保持回溯相容性,仍然接受 int
7.4.0 sodium 擴充功能為 Argon2 密碼提供替代實作。
7.3.0 新增使用 PASSWORD_ARGON2ID 支援 Argon2id 密碼。
7.2.0 新增使用 PASSWORD_ARGON2I 支援 Argon2i 密碼。

範例

範例 #1 password_hash() 範例

<?php
/**
* 我們只想使用目前的 DEFAULT 演算法雜湊密碼。
* 目前是 BCRYPT,並且會產生 60 個字元的結果。
*
* 請注意,DEFAULT 可能會隨著時間而變更,因此您最好準備
* 讓您的儲存空間擴展到 60 個字元以上(255 個字元會不錯)
*/
echo password_hash("rasmuslerdorf", PASSWORD_DEFAULT);
?>

上述範例會輸出類似以下的內容

$2y$10$.vGA1O9wmRjrwAVXD98HNOgsNpDczlqm3Jq7KnEd1rVAGv3Fykk1a

範例 #2 password_hash() 手動設定成本的範例

<?php
/**
* 在這個範例中,我們想要將 BCRYPT 的預設成本增加到 12。
* 請注意,我們也切換到 BCRYPT,它將永遠是 60 個字元。
*/
$options = [
'cost' => 12,
];
echo
password_hash("rasmuslerdorf", PASSWORD_BCRYPT, $options);
?>

上述範例會輸出類似以下的內容

$2y$12$QjSH496pcT5CEbzjD/vtVeH03tfHKFy36d4J0Ltp3lRtee9HDxY3K

範例 #3 password_hash() 範例:尋找適合的成本

<?php
/**
* 這段程式碼將會為您的伺服器進行基準測試,以判斷您可以負擔的最高成本。
* 您希望設定在不會過度拖慢伺服器速度的情況下,盡可能最高的成本。
* 10 是一個良好的基準,如果您的伺服器速度夠快,則可以設定更高的值。
* 下面的程式碼旨在達成 ≤ 350 毫秒的延展時間,
* 這對於處理互動式登入的系統來說是一個適當的延遲。
*/
$timeTarget = 0.350; // 350 毫秒

$cost = 10;
do {
$cost++;
$start = microtime(true);
password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
$end = microtime(true);
} while ((
$end - $start) < $timeTarget);

echo
"找到合適的成本: " . $cost;
?>

上述範例會輸出類似以下的內容

Appropriate Cost Found: 12

範例 #4 password_hash() 範例:使用 Argon2i

<?php
echo 'Argon2i 雜湊值:' . password_hash('rasmuslerdorf', PASSWORD_ARGON2I);
?>

上述範例會輸出類似以下的內容

Argon2i hash: $argon2i$v=19$m=1024,t=2,p=2$YzJBSzV4TUhkMzc3d3laeg$zqU/1IN0/AogfP4cmSJI1vc8lpXRW9/S0sYY2i2jHT0

注意事項

注意

強烈建議您不要為此函數產生自己的鹽 (salt)。如果您未指定鹽,它會自動為您建立安全的鹽。

如上所述,在 PHP 7.0 中提供 salt 選項將會產生棄用警告。在 PHP 8.0 中,已移除手動提供鹽的支援。

注意:

建議您在您的伺服器上測試此函數,並調整成本參數,使該函數在互動式系統上的執行時間少於 350 毫秒。上述範例中的腳本將會協助您為您的硬體選擇一個好的成本值。

注意 此函數支援的演算法更新(或預設演算法的變更)必須遵循以下規則:

  • 任何新的演算法必須在成為預設演算法之前,至少在 PHP 的一個完整版本中存在。因此,舉例來說,如果一個新的演算法在 7.5.5 中加入,則在 7.7 之前(因為 7.6 會是第一個完整版本)都不會被選為預設演算法。但如果一個不同的演算法在 7.6.0 中加入,那麼它也將在 7.7.0 中可以被選為預設演算法。
  • 預設演算法只應在完整版本 (7.3.0、8.0.0 等) 中變更,而不應在修訂版本中變更。唯一的例外情況是在當前的預設演算法中發現嚴重的安全漏洞時,才會緊急變更。

另請參閱

新增註解

使用者貢獻的註解 8 則註解

phpnetcomment201908 at lucb1e dot com
5 年前
自 2017 年以來,NIST 建議在雜湊記憶的密碼等機密資訊時使用秘密輸入。透過混入秘密輸入(通常稱為「胡椒」),即使攻擊者擁有雜湊值和鹽,也能防止他們徹底暴力破解密碼雜湊。例如,SQL 注入通常只影響資料庫,不會影響磁碟上的檔案,因此儲存在設定檔中的胡椒仍然超出攻擊者的範圍。胡椒必須隨機產生一次,並且對於所有使用者都可以相同。如果網站擁有者這樣做,許多密碼洩漏事件可能會完全無效。

由於 password_hash 沒有胡椒參數(即使 Argon2 有一個「secret」參數,PHP 也不允許設定它),混入胡椒的正確方法是使用 hash_hmac()。php.net 的「新增註解」規則說我不能連結外部網站,所以我無法用 NIST、維基百科、來自安全堆疊交換網站的解釋理由的文章等連結來支持任何這些說法... 您必須手動驗證。程式碼

// config.conf
pepper=c1isvFdxMDdmjOlvxpecFw

<?php
// register.php
$pepper = getConfigVariable("pepper");
$pwd = $_POST['password'];
$pwd_peppered = hash_hmac("sha256", $pwd, $pepper);
$pwd_hashed = password_hash($pwd_peppered, PASSWORD_ARGON2ID);
add_user_to_database($username, $pwd_hashed);
?>

<?php
// login.php
$pepper = getConfigVariable("pepper");
$pwd = $_POST['password'];
$pwd_peppered = hash_hmac("sha256", $pwd, $pepper);
$pwd_hashed = get_pwd_from_db($username);
if (
password_verify($pwd_peppered, $pwd_hashed)) {
echo
"密碼符合。";
}
else {
echo
"密碼不正確。";
}
?>

請注意,此程式碼包含一個洩漏使用者名稱是否存在的時間攻擊。但是我的註解超過了長度限制,所以我不得不刪除這段文字。

另請注意,如果洩漏或可以破解,胡椒是無用的。考慮一下它可能如何暴露,例如將它傳遞給 docker 容器的不同方法。為了防止破解,請使用一個長的隨機產生值(如上面的範例),並在您使用乾淨的使用者資料庫進行新安裝時變更胡椒。變更現有資料庫的胡椒與變更其他雜湊參數相同:您可以將舊值包裝在新值中並分層雜湊(更複雜),您可以在有人登入時計算新的密碼雜湊(使舊使用者處於風險之中,因此這可能沒問題,具體取決於您升級的原因)。

為什麼會這樣?因為攻擊者在竊取資料庫後會執行以下操作

password_verify("a", $stolen_hash)
password_verify("b", $stolen_hash)
...
password_verify("z", $stolen_hash)
password_verify("aa", $stolen_hash)
等等。

(更實際的做法是,他們使用破解字典,但原則上,破解密碼雜湊的方式是猜測。這就是我們使用特殊演算法的原因:它們速度較慢,因此每個 verify() 操作都會較慢,因此他們每小時破解的密碼會少很多。)

現在,如果您使用了胡椒呢?現在他們需要做的是

password_verify(hmac_sha256("a", $secret), $stolen_hash)

沒有那個 $secret(胡椒),他們就無法進行此計算。他們必須做

password_verify(hmac_sha256("a", "a"), $stolen_hash)
password_verify(hmac_sha256("a", "b"), $stolen_hash)
...
等等,直到他們找到正確的胡椒。

如果您的胡椒包含 128 位元的熵,並且只要 hmac-sha256 保持安全(即使 MD5 在 hmac 中使用在技術上是安全的:只有其衝突抗性被破壞,但當然沒有人會使用 MD5,因為發現的缺陷越來越多),這將比太陽輸出的能量還要多。換句話說,目前不可能破解如此強大的胡椒,即使已知密碼和鹽也是如此。
bhare at duck dot com
1 年前
如果您要使用 bcrypt,那麼您應該使用隨機的大字串來加胡椒密碼,因為普通的硬體可以在一個小時內破解 bcrypt 8 個字元的密碼;https://www.tomshardware.com/news/eight-rtx-4090s-can-break-passwords-in-under-an-hour
nicoSWD
11 年前
我同意 martinstoeckli 的觀點,

除非你真的知道自己在做什麼,否則不要建立自己的鹽值 (salt)。

預設情況下,它會使用 /dev/urandom 來建立鹽值,而這基於裝置驅動程式的雜訊。

在 Windows 上,它會使用 CryptGenRandom()。

兩者都已存在多年,並被認為在密碼學上是安全的(前者可能比後者更安全)。

不要試圖透過建立安全性較低的東西來超越這些預設值。任何基於 rand()、mt_rand()、uniqid() 或這些變體的東西都*不*好。
Lyo Mi
8 年前
請注意,password_hash 會在第一個 NULL 位元組處***截斷***密碼。

http://blog.ircmaxell.com/2015/03/security-issue-combining-bcrypt-with.html

如果你使用任何會產生 NULL 位元組的輸入(sha1 的 raw 設為 true,或者 NULL 位元組可能自然出現在人們的密碼中),你可能會讓你的應用程式比你預期的更不安全。

密碼
$a = "\01234567";
對於 bcrypt 來說是零位元組長度(一個空密碼)。

當然,解決方法是確保你永遠不會將 NULL 位元組傳遞給 password_hash。
fullstadev at gmail dot com
6 個月前
與這裡另一個關於在 password_hash() 中使用包含 null 位元組的字串的貼文類似,我想更精確一點,因為我們現在遇到了一些問題。

我有一個應用程式產生隨機雜湊值(CSPRN)的專案。他們所做的是使用了 random_bytes(32),並將 password_hash() 應用於獲得的字串,使用 bcrypt 演算法。

一方面,這導致了有時 random_bytes() 會產生包含 null 位元組的字串,實際上導致對 password_hash() 的呼叫出現錯誤(PHP v 8.2.18)。由於這個錯誤訊息("Bcrypt 密碼不得包含 null 字元"),我修改了產生隨機雜湊值的函式,使用 bin2hex()(或 base64 或任何其他方式)對使用 random_bytes() 獲得的二進位隨機字串進行編碼,以確保要雜湊的字串沒有 null 位元組。

然後我只想補充一點,當你使用 bcrypt 演算法時,請務必記住 bcrypt 會將你的密碼截斷為 72 個字元。當對你的隨機字串(例如使用 random_bytes() 產生)進行編碼時,這會將你的字串從二進位轉換為十六進位表示,例如使其長度加倍。你通常希望你的整個密碼仍然包含在 72 個字元的限制內,以確保你的整個「隨機資訊」都被雜湊,而不僅僅是一部分。
martinstoeckli
11 年前
在大多數情況下,最好省略 salt 參數。如果沒有這個參數,該函數會從作業系統的隨機來源產生一個密碼學上安全的鹽值。
ms1 at rdrecs dot com
5 年前
簡單來說,時序攻擊是指可以透過執行速度計算出密碼的哪些字元的攻擊。

更多資訊請參考...
https://paragonie.com/blog/2015/11/preventing-timing-attacks-on-string-comparison-with-double-hmac-strategy

我已加入程式碼以回應 phpnetcomment201908 at lucb1e dot com 的建議,使用 phpnetcomment201908 at lucb1e dot com 發布的程式碼,使這種可能的「時序攻擊」更加困難。

$pph_strt = microtime(true);

//...
/* 他發布的 login.php 程式碼 */
//...

$end = (microtime(true) - $pph_strt);

$wait = bcmul((1 - $end), 1000000); // usleep(250000) 1/4 秒

usleep ( $wait );

echo "<br>執行時間:".(microtime(true) - $pph_strt)."; ";

請注意,我建議根據你的需求更改等待時間,但請確保它大於你的伺服器上腳本所花費的最高執行時間。

此外,這是我的解決方法,用來模糊執行時間,以消除時序攻擊。你可以在我發布的連結中找到更深入的討論以及更多來自比我更精通密碼學的人的資訊。我不認為那裡有這個(指延遲),但還有其他的。這是我了解什麼是時序攻擊的地方,因為我是新手,但希望有紮實的安全性。
Mike Robinson
10 年前
對於密碼,你通常希望雜湊計算時間在 250 到 500 毫秒之間(管理員帳戶可能需要更多)。由於計算時間取決於伺服器的效能,因此在兩個不同的伺服器上使用相同的 cost 參數可能會導致執行時間差異很大。這裡有一個小函數可以幫助你確定你的伺服器應該使用哪個 cost 參數,以確保你在此範圍內(請注意,我提供了一個鹽值以消除建立虛擬隨機鹽值所造成的任何延遲,但在雜湊密碼時不應該這樣做)

<?php
/**
* @Param int $min_ms 計算雜湊值所需的最短時間(以毫秒為單位)
*/
function getOptimalBcryptCostParameter($min_ms = 250) {
for (
$i = 4; $i < 31; $i++) {
$options = [ 'cost' => $i, 'salt' => 'usesomesillystringforsalt' ];
$time_start = microtime(true);
password_hash("rasmuslerdorf", PASSWORD_BCRYPT, $options);
$time_end = microtime(true);
if ((
$time_end - $time_start) * 1000 > $min_ms) {
return
$i;
}
}
}
echo
getOptimalBcryptCostParameter(); // 在我的情況下印出 12
?>
To Top