我已经搜索了SO,我知道有很多" Remember Me "问题,但我找不到一个专门关于使用Blowfish生成安全令牌的问题。
我正在实现一个登录系统,使用基于cookie的"记住我"功能,我读过的所有教程要么看起来不安全(例如,只是基于时间的无盐md5哈希),要么看起来过于复杂。
我不是在为银行或类似的东西建网站,只是想要一个合理的安全级别。
我建议的系统是简单地为用户名创建一个128个字符的随机字符串,为登录令牌创建第二个128个字符的字符串。原始字符串将存储在cookie中,未加盐的sha1ed版本将存储在数据库中用户帐户的行中。我想我甚至可以在每次页面加载时重新生成字符串。
对我来说,这提供了不错的安全性(我认为!)因为:
- 黑客在知道用户名(他们需要知道128个字符的字符串)的情况下无法攻击特定的帐户
- 要伪造登录cookie并获得访问帐户的权限,某人必须猜测2 x 128个字符串。
- 如果数据库被黑客入侵,为128个字符串创建彩虹表太难了。
- 我可以安全地记住用户名只有存储在cookie的实际用户名。
我的问题是:
- 这听起来合理安全吗?我应该使用超过128个字符的字符串吗?
- 是否值得使用Blowfish而不是sha1?(也就是说,我说彩虹表太难制作了吗?)
- 如果没有,运行sha1 1000次有什么好处吗?
多谢。
河豚可能意味着两件事。Cipher(双向加密算法)或Hash(单向散列算法,具体称为bcrypt)。但更多的是在一点。
那么让我们来看看你假设的优势。
-
黑客无法通过知道用户名来攻击特定的帐户。
即使他们知道用户名,也没关系。128位随机字符串足够大,您真的不需要担心攻击者会猜到它。让我们做一些数学。
2^128 == 3 * 10^38 possible combinations Assuming there are 50 million servers on the internet, And each server can do 100,000,000,000 guesses per second (to your server) 3e38 / 50,000,000 / 100,000,000,000 == 6 * 10^19 seconds Which when converted to years is: 1,902,587,519,025 To have a 50% chance of guessing it, the attacker would need to hit 1/2: Years to 50% chance of guessing: 951,293,759,512
因此,除非你担心攻击者试图在未来万亿年内闯入你的系统,否则128位足够强大…
-
伪造登录cookie…必须猜测2 x 128个字符字符串
我们已经证明了猜1是不会发生的。所以没有必要添加第二秒(它可能不坏,但不需要)
-
如果数据库被黑客入侵,为128个字符串创建彩虹表太难了
是的。然而,这不是你应该担心的。你应该担心的是蛮力。
那么,回答你实际的问题:
-
这听起来很安全吗?
,合理确定。过于复杂的:当然。有更简单的方法来处理这个问题…
-
值得用Blowfish代替sha1吗?
。Blowfish是用于衍生(意思是你想要一个工作量证明)。在本例中,您希望生成MAC(机器身份验证代码)。所以我不会使用Blowfish或SHA1。我会使用SHA256或SHA512…
-
运行sha1 1000次有什么好处?
没有
更好的方法
我推荐的更好的方法是将cookie分成三部分存储。
function onLogin($user) {
$token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
storeTokenForUser($user, $token);
$cookie = $user . ':' . $token;
$mac = hash_hmac('sha256', $cookie, SECRET_KEY);
$cookie .= ':' . $mac;
setcookie('rememberme', $cookie);
}
然后,验证:
function rememberMe() {
$cookie = isset($COOKIE['rememberme']) ? $COOKIE['rememberme'] : '';
if ($cookie) {
list ($user, $token, $mac) = explode(':', $cookie);
if ($mac !== hash_hmac('sha256', $user . ':' . $token, SECRET_KEY)) {
return false;
}
$usertoken = fetchTokenByUserName($user);
if (timingSafeCompare($usertoken, $token)) {
logUserIn($user);
}
}
}
现在,非常重要的是SECRET_KEY
是一个加密秘密(由/dev/random
之类的东西生成和/或从高熵输入派生)。另外,GenerateRandomToken()
需要是强随机源(mt_rand()
还不够强)。
和timingSafeCompare是防止定时攻击。像这样:
/**
* A timing safe equals comparison
*
* To prevent leaking length information, it is important
* that user input is always used as the second parameter.
*
* @param string $safe The internal (safe) value to be checked
* @param string $user The user submitted (unsafe) value
*
* @return boolean True if the two strings are identical.
*/
function timingSafeCompare($safe, $user) {
// Prevent issues if string length is 0
$safe .= chr(0);
$user .= chr(0);
$safeLen = strlen($safe);
$userLen = strlen($user);
// Set the result to the difference between the lengths
$result = $safeLen - $userLen;
// Note that we ALWAYS iterate over the user-supplied length
// This is to prevent leaking length information
for ($i = 0; $i < $userLen; $i++) {
// Using % here is a trick to prevent notices
// It's safe, since if the lengths are different
// $result is already non-0
$result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}
据我所知,关键的一点是如何生成随机标记,以及在这个字符串中允许使用什么字母。假设您使用操作系统的随机源来生成这个128个字符的字符串,并且允许区分大小写的字符和数字,那么您将得到一个非常强大且不可预测的"密码"。
虽然我的感觉告诉我使用BCrypt(没有理由反对它),但纯粹的逻辑说:
- 128个字符比通常的加盐密码长得多,所以不需要加盐。
- 不可能暴力破解62^128 = 2E229个组合。字典攻击也没有问题,因为令牌的随机性。这意味着不需要迭代哈希函数。
我不会使用SHA-1,但这会将您的安全令牌的熵减少到160位。创建一个超强令牌是没有意义的,只存储它的一部分,而是使用SHA-256或SHA-512。
还有其他事情要考虑,配置cookie,所以它只能通过HTTPS发送。实际上,您将面临与维护会话相同的问题。
如果您使用SSL创建安全连接来交换令牌,那么我会说这是相当安全的。我只会使用guid之类的东西作为令牌,而不是blowfish或SHA1。当你真正想要的是一个大的随机比特串时,这些真的没有得到任何东西。
如果你没有在第一个地方创建一个安全的连接,那么中间人攻击很可能很容易完成。由于SSL每次都创建一个随机密钥,因此没有必要在每次加载页面时更改令牌。