在这篇文章中,我将解释如何安全的存储用户密码,一些示例代码可能使用 Python 语言展示,并使用了 Bcrypt 库。

糟糕的解决方案:纯文本密码

将每个用户的“纯文本”密码存储到数据库中是非常不安全的:

账号密码明文
john#hotmail.compassword
betty#gmail.compassword123
......

这是不安全的,如果一个黑客获得访问数据库的权限后,他们可以用这个用户名和密码轻易的登录到你的系统上。甚至更糟

的是,如果用户在其他网站上使用了相同的密码,黑客也可以使用这个用户名和密码去登录。这将给你的用户造成巨大的损失。

曾经最有名的明文密码泄漏事件就是CSDN网站六百万用户信息外泄。

糟糕的解决方案:sha1(password)

相对于明文密码来说,一个更好的解决方案是使用“单向散列”(one-way hash,又称单向Hash)存储密码,通常使md5() 或 sha1() 这样的函数。

账号sha1(password)
john#hotmail.com5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8
betty#gmail.comcbfdac6008f9cab4083784cbd1874f76618d2a97
......

即使服务器不保存纯文本密码,它仍然可以对用户进行身份验证:

def is_password_correct(user, password_attempt):
    return sha1(password_attempt) == user["sha1_password"]

这个解决方案比储存纯文本密码更安全,因为理论上应该不可能“undo”单向散列函数并且找到一个相同的散列值的字符串。不幸的是,黑客找到了破解它的方法。

一个问题是,许多哈希函数(包括 md5()sha1() )毕竟不那么“单向”,安全专家建议不应该在涉及安全性的应用程序中继续使用这些函数(相反,你应该用更好的哈希函数,像 sha256() 这样,到目前为止还没有发现任何已知的漏洞)。

知名职业社交网站LinkedIn(领英)的密码就是使用sha1(password)的方式存储。在2012年,大量的LinkedIn密码散列外泄。随后,1.17亿的LinkedIn密码被破解。

总结:存储一个简单的散列(无盐)是不安全的,如果一个黑客获得访问数据库的权限,他们就能够计算出大多数用户的密码。

糟糕的解决方案:sha1(FIXED_SALT + password)

相对于上一种解决方案,更加安全和稳固的做法是把密码加salt(也叫加盐)之后再散列:

账号sha1("salt123456789" + password)
john#hotmail.comb467b644150eb350bbc1c8b44b21b08af99268aa
betty#gmail.com31aa70fd38fee6f1f8b3142942ba9613920dfea0
......

在这里“盐”是一个固定的、很长的随机字符串。如果黑客获得这些新密码散列(但没获取到“盐”),这将使黑客破解密码变得更加困难,因为他们需要知道盐才能破解。然而,如果黑客闯入你的服务器,他们可能也获得源代码,所以他们也会获取“盐”值。这就是为什么安全设计师只是做最坏的打算,并且不盲目的相信“盐”是保密的。

但即使“盐”值并不再保密,它仍然可以增加传统的彩虹表破解法的难度(假设没有使用“盐”建立彩虹表,加盐散列能够阻止他们)。

然而,即使没有彩虹表,一个固定的“盐”也不会对安全有什么帮助。获取“盐”值之后,黑客仍然可以执行下面的循环:

for password in LIST_OF_COMMON_PASSWORDS:
    if sha1(SALT + password) in database_table:
        print "password is: ", password

总结:即使添加一个固定的“盐”,但还不够安全。

糟糕的解决方案:sha1(PER_USER_SALT + password)

一个更安全的做法是在数据库中创建一个新列,为每个用户存储不同的“盐”。“盐”在用户第一次创建帐户时随机生成(或当用户更改密码时随机改变)。

账号saltsha1(salt + password)
john#hotmail.com2dc7fcc...1a74404cb136dd60041dbf694e5c2ec0e7d15b42
betty#gmail.comafadb2f...e33ab75f29a9cf3f70d3fd14a7f47cd752e9c550
.........

验证用户并不比以前难:

def is_password_correct(user, password_attempt):
    return sha1(user["salt"] + password_attempt) == user["password_hash"]

设置每个用户使用不同的“盐”有一个巨大的好处:黑客不能在同一时间攻击你的所有用户的密码。取而代之的是,他的攻击代码必须一个接一个的尝试破解每个用户:

for user in users:
    PER_USER_SALT = user["salt"]

    for password in LIST_OF_COMMON_PASSWORDS:
        if sha1(PER_USER_SALT + password) in database_table:
            print "password is: ", password

因此,如果你有100万用户,每个用户使用不同的“盐”可以使算出所有用户的密码的难度增加100万倍。但这仍然不是一个黑客无法做到的事。假设以前需要1 cpu-hour,现在需要100万cpu-hours,花费40000美元在亚马逊租用服务器可以很容易的解决这件事。

所有的系统的真正问题到目前为止,我们已经讨论了像sha1哈希函数()(甚至sha256())可以在执行密码100 + /秒的速度(或更快,利用GPU)。

到目前为止,我们讨论的所有系统的真正的问题是像 sha1()(或甚至sha256())这样的哈希函数可以在100M+/秒(通过使用GPU甚至更快 )的速度上计算密码。即使这些哈希函数在设计时考虑到安全性,它们也被设计成当在诸如整个文件的较长输入上执行时也是快速的。

总结:这些哈希函数并非设计用于密码存储。

优秀的解决方案:bcrypt(password)

相反,有一组专门为密码设计的散列函数。 除了是安全的“单向”散列函数之外,它们的计算速度也被设计得很慢,专门设计用于很难在GPU上实现。

一个例子是 Bcrypt算法bcrypt()需要花费大约100ms来计算,这比sha1()大约慢10,000倍。100ms足够快,用户在登录时不会注意到,但速度足够也慢,对于大量的密码执行变得不太可行。例如,如果一个黑客想要对一个十亿的密码列表计算bcrypt(),它将需要大约30,000 cpu小时 - 这是一个单一的密码。 当然不是不可能,但这样大多数黑客都需要做更多的工作。

同时,Bcrypt是可配置的,可以使用log_rounds参数告诉它要执行多少次内部哈希函数。如果突然间,英特尔推出了新一代CPU,速度是现在的1000倍,你可以重新设置系统中的log_rounds参数,在原来的基础上加10(log_rounds是对数),这将可以抵消1000倍的计算速度。

虽然bcrypt()算法计算缓慢,但是也可以使用彩虹表破解,所以Bcrypt系统为每个用户都创建了一个“盐”值。实际上一些开源bcrypt算法库将“盐”值和密码Hash存储到一个字段中,所以你不需要在数据库中创建一个专门用来存储每个用户的“盐”值的字段。

以下是使用Python代码生成一个新密码的过程:

from bcrypt import hashpw, gensalt
hashed = hashpw(plaintext_password, gensalt())
print hashed    # save this value to the database for this user
'$2a$12$8vxYfAWCXe0Hm4gNX8nzwuqWNukOkcMJ1a9G2tD71ipotEZ9f80Vu'

让我们仔细分析一下输出字符串:

bcrypt_output_string2

正如你所看到的,它在字符串中存储“盐”值和密码哈希。 它还存储用于生成密码的log_rounds参数,它控制要计算多少工作(越大越慢)。 如果你想要的哈希算法更慢,你传递一个更大的值log_rounds值到gensalt()函数中:

hashed = hashpw(plaintext_password, gensalt(log_rounds=13))
print hashed
'$2a$13$ZyprE5MRw2Q3WpNOGZWGbeG7ADUre1Q8QO.uUUtcbqloU0yvzavOm'

注意,现在log_rounds的值是13 (之前是12)。 在任何情况下,您都将此字符串存储在数据库中,并且当同一用户尝试登录时,您检索相同的散列值,并执行以下操作:

if cheakhashpw(password_attempt, hashed) == hashed:
    print "It matches"
else:
    print "It does not match"

结论

为了保护用户的密码安全,至少应该做到以下几点:

  • 确保你的用户使用的密码是一个强壮的密码(例如:密码长度大于12位,使用字母、数组、和特殊字母的组合)。
  • bcrypt 或 PBKDF2 等专门为加密密码设计的算法。

目前已知的专门为密码设计的算法有:

bcrypt算法的 C/C++ 实现可以查看:

Argon2scrypt算法可以使用 C/C++ 开源库 libsodium 实现。

参考文章:
simple bcrypt library for C
Speed Hashing

标签: 安全, 密码, bcrypt, sha1

添加新评论