如何生成一个好的盐 - 我的函数是否足够安全?

40

这是我正在使用的生成随机盐的函数:

function generateRandomString($nbLetters){
    $randString="";
    $charUniverse="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    for($i=0; $i<$nbLetters; $i++){
       $randInt=rand(0,61);
        $randChar=$charUniverse[$randInt];
        $randString=$randomString.$randChar;
    }
    return $randomString;
}

这是用于非商业网站的。它只用于生成盐(将其存储在数据库中,并与用户提交的密码一起用于散列)。

这样做合适吗?我应该使用更大的字符子集,如果需要,PHP 中有简便的方法可以实现吗?

11个回答

48
如果您正在进行密码散列,应使用现代的散列算法,无需生成自己的salt。使用弱哈希算法对您和用户都存在危险。我的原始答案是八年前写的。时代已经改变了,现在密码散列要容易得多。
您应该始终使用内置函数来散列/检查密码。在任何时候使用自己的算法都会引入大量不必要的风险。
对于PHP,请考虑使用password_hash(), 采用PASSWORD_BCRYPT算法。无需提供自己的salt。
以下是我原始的答案,供后人参考:

警告:根据uniqid的文档,以下实现不会产生不可预测的盐。

来自php sha1页面

$salt = uniqid(mt_rand(), true);

这看起来比你提出的方案更简单、更有效(因为每个都是独特的)。


18

如果你使用的是Linux,那么/dev/urandom可能是你最好的随机数源。它由操作系统本身提供,因此比任何PHP内置函数更可靠。

$fp = fopen('/dev/urandom', 'r');
$randomString = fread($fp, 32);
fclose($fp);
这将给你32个字节的随机blob。你可能想要通过类似于base64_encode()的函数来使其可读。没有必要自己操作字符。
编辑2014年:在PHP 5.3及以上版本中,openssl_random_pseudo_bytes()是获取一堆随机字节的最简单方法。在*nix系统上,它在幕后使用/dev/urandom。在Windows系统上,它使用内置于OpenSSL库中的不同算法。
相关:https://security.stackexchange.com/questions/26206 相关:should i use urandom or openssl_random_pseudo_bytes?

我同意这个观点。原帖使用了一个非加密安全的伪随机生成器。我只想补充一点,盐可以以二进制形式存储在数据库中,以减少base64编码/解码延迟,因为哈希应该对身份验证登录透明。此外,如果服务器是IIS/Win32,则mcrypt_create_iv()函数(如果可用)将汇集win32 cryptoAPI,如果您需要在该场景中获得良好的随机位而不想处理COM接口(CAPICOM或其他)。由于原帖询问了一个随机盐,我认为@kijin的答案是最佳解决方案。 - DrPerdix
+1 这是页面上最好的答案,尽管我不会使用 base64_encode。请记住,所有消息摘要函数都是二进制安全的,并且您需要一个完整的字节盐。字母数字彩虹表非常普遍,但是基于256的彩虹表则闻所未闻。 - rook
@Rook 当然,您可以将任何二进制字符串传递给哈希函数。但通常盐也与哈希一起存储。(否则您将如何将哈希与密码进行比较呢?) 大多数人倾向于在诸如MySQL之类的数据库中将整个内容存储在文本列中。文本列不接受任意二进制数据,因此您必须将其转换为blob列或使用类似base64的东西。这只是使您的盐变长了;加密强度是相同的。 - kijin
@kijin 我明白你的意思。但是如果在插入之前转义二进制,它就会保留值。你甚至可以使用 mysql_real_escape_string() 将图像存储在数据库中。 - rook
@Rook 即使你逃避它,blob 是二进制值(如图像)的正确数据类型。文本列附有字符集,这可能会因使用的 DBMS 而破坏您的二进制数据。我想提供一个与通常分配给密码字段的 varchar/text 类型相容的答案,因此我坚持我的建议,即任何二进制 salt 应该是 base64 编码的。如果您想使用 blob 来节省一些磁盘空间,那没问题。 - kijin
显示剩余2条评论

8

password_hash() 在 PHP 5.5 及更高版本中可用。我很惊讶这里没有提到它。

使用 password_hash(),无需生成盐,因为盐是自动使用 bcrypt 算法生成的 —— 因此不需要编写一组字符。

相反,将用户提交的密码与存储在数据库中的唯一密码哈希值进行比较,可以使用 password_verify()。只需在用户数据库表中存储用户名和密码哈希值,就可以使用 password_verify() 将其与用户提交的密码进行比较。

password_hash() 的工作原理:

password_hash() 函数输出一个唯一的密码哈希值,当将该字符串存储在数据库中时——建议该列允许最多 255 个字符。

$password = "goat";
echo password_hash($password, PASSWORD_DEFAULT);
echo password_hash($password, PASSWORD_DEFAULT);
echo password_hash($password, PASSWORD_DEFAULT);

// Output example (store this in the database)
$2y$10$GBIQaf6gEeU9im8RTKhIgOZ5q5haDA.A5GzocSr5CR.sU8OUsCUwq  <- This hash changes.
$2y$10$7.y.lLyEHKfpxTRnT4HmweDKWojTLo1Ra0hXXlAC4ra1pfneAbj0K
$2y$10$5m8sFNEpJLBfMt/3A0BI5uH4CKep2hiNI1/BnDIG0PpLXpQzIHG8y 

要验证哈希密码,您可以使用password_verify()函数:

$password_enc = password_hash("goat", PASSWORD_DEFAULT);
dump(password_verify('goat', $password_enc)); // TRUE
dump(password_verify('fish', $password_enc)); // FALSE

如果您愿意,可以手动添加盐作为选项,如下所示:
$password = 'MyPassword';
$salt = 'MySaltThatUsesALongAndImpossibleToRememberSentence+NumbersSuch@7913';
$hash = password_hash($password, PASSWORD_DEFAULT, ['salt'=>$salt]);
// Output: $2y$10$TXlTYWx0VGhhdFVzZXNBT.ApoIjIiwyhEvKC9Ok5qzVcSal7T8CTu  <- This password hash not change.

2
这个答案很好,因为它涵盖了生成密码的最新PHP实现。但它没有回答如何生成一个好的盐(有时你需要明确这一点,例如用于其他哈希目的)。 - Gottlieb Notschnabel
生成一个新的盐非常简单:$salt = password_hash("goat", PASSWORD_DEFAULT); 答案已更新。 - Kristoffer Bohmann
@KristofferBohmann - 这种方式并不能创建盐,其结果是密码哈希值。这样生成盐会非常浪费 CPU 资源,并且这些“盐”也不是最优的。 - martinstoeckli

4
rand(0,61) 替换为 mt_rand(0, 61),你的程序就能正常运行了(因为mt_rand更适合生成随机数)...
但比盐值强度更重要的是哈希方式。如果你的盐值处理过程很好,但只使用md5($pass.$salt),那么你白费了盐值的作用。我个人建议对哈希值进行拉伸...例如:
function getSaltedHash($password, $salt) {
    $hash = $password . $salt;
    for ($i = 0; $i < 50; $i++) {
        $hash = hash('sha512', $password . $hash . $salt);
    }
    return $hash;
}

如果想了解有关哈希伸展的更多信息,请查看此SO答案 ...


我的函数是:function stringHashing($saltedString){$hashedString=hash('sha256',$saltedString); return $hashedString; } - JDelage
@ircmaxell 这需要更新吗,还是现在仍然相关? - JayIsTooCommon

4
我会采纳另一个回答中提到的建议,使用mt_rand(0, 61),因为Mersenne Twister生成更好的熵。
此外,您的函数实际上分为两部分:生成随机的$nbLetters数字,并将其编码为base62。这将使维护程序员(也许是您!)在几年后遇到它时更加清晰明了。
// In a class somewhere
private $chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

private function getBase62Char($num) {
    return $chars[$num];
}

public function generateRandomString($nbLetters){
    $randString="";

    for($i=0; $i < $nbLetters; $i++){
        $randChar = getBase62Char(mt_rand(0,61));
        $randString .= $randChar;
    }

    return $randomString;
}

2
如果您使用的是Windows系统而无法使用/dev/random,这里有一个更好的方法。
//Key generator
$salt = base64_encode(openssl_random_pseudo_bytes(128, $secure));
//The variable $secure is given by openssl_random_ps... and it will give a true or false if its tru then it means that the salt is secure for cryptologic.
while(!$secure){
    $salt = base64_encode(openssl_random_pseudo_bytes(128, $secure));
}

2

这是我的方法,它使用来自大气噪声的真正随机数。它与伪随机值和字符串混合在一起。经过洗牌和哈希处理。以下是我的代码:我称之为过度设计。

<?php
function generateRandomString($length = 10) {
    $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    $randomString = '';
    for ($i = 0; $i < $length; $i++) {
        $randomString .= $characters[rand(0, strlen($characters) - 1)];
    }
    return $randomString;
}

function get_true_random_number($min = 1, $max = 100) {
    $max = ((int) $max >= 1) ? (int) $max : 100;
    $min = ((int) $min < $max) ? (int) $min : 1;
    $options = array(
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HEADER => false,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_ENCODING => '',
        CURLOPT_USERAGENT => 'PHP',
        CURLOPT_AUTOREFERER => true,
        CURLOPT_CONNECTTIMEOUT => 120,
        CURLOPT_TIMEOUT => 120,
        CURLOPT_MAXREDIRS => 10,
    );

    $ch = curl_init('http://www.random.org/integers/?num=1&min='
        . $min . '&max=' . $max . '&col=1&base=10&format=plain&rnd=new');
    curl_setopt_array($ch, $options);
    $content = curl_exec($ch);
    curl_close($ch);

    if(is_numeric($content)) {
        return trim($content);
    } else {
        return rand(-10,127);
    }
}

function generateSalt() {
    $string = generateRandomString(10);
    $int = get_true_random_number(-2,123);
    $shuffled_mixture = str_shuffle(Time().$int.$string);
    return $salt = md5($shuffled_mixture);
}

echo generateSalt();
?>

大气噪声由random.org提供。我还见过通过熔岩灯的图像解释来产生真正的随机生成。(色调是位置)

1
一个相当简单的技巧:

$a = array('a', 'b', ...., 'A', 'B', ..., '9');
shuffle($a);
$salt = substr(implode($a), 0, 2);  // or whatever sized salt is wanted

与uniqid()不同,它生成随机结果。

1

我认为一个非常好的盐值例子是用户名(如果你正在谈论密码哈希并且用户名不会改变)。

你不需要生成任何东西,也不需要存储更多的数据。


不是一个坏主意,但你可能至少想先对用户名执行一个微不足道的函数。可以简单地重复两次,或在末尾添加一个字符。虽然你建议的数学上是正确的,但很容易被猜到。 - jon_darkstar
4
谁在意它是否能被猜测呢?盐的目的是防止在数据库中使用一张彩虹表破解所有哈希值。它已经做到了这一点。 - NikiC
是的,没错。盐甚至可以是公开的,重要的是每个密码都有独特的盐。 - Gottlieb Notschnabel

1
我使用这个:

$salt = base64_encode(mcrypt_create_iv(PBKDF2_SALT_BYTES, MCRYPT_DEV_URANDOM));

Base64编码还会返回字符“+”和“/”,这也许不是提问者所需的。然后需要为特定算法创建一个给定长度的盐值,你的例子中长度固定且难以预测。 - martinstoeckli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接