Web 开发中的盐、哈希和安全性

Salt, hashes and security in web development

Udacity.com 有一门名为 Web 开发的课程。 讲师向我们介绍了 cookie、如何签署 cookie、什么是盐以及我们必须始终在数据库中保留密码的哈希值。然后他给了我们作业,然后他发布了一个解决方案。 只有在这里我们才看到了全貌:它们应该如何协同工作。

下面是 Python 和 Google App Engine 的代码。但是,请不要害怕。该代码似乎像伪代码一样可读。 而且问题是笼统的,不是关于这个特殊技术的。

import hashlib
import hmac

secret = 'iamsosecret'

def make_secure_val(val):
    return '%s|%s' % (val, hmac.new(secret, val).hexdigest())

def check_secure_val(secure_val):
    val = secure_val.split('|')[0]
    if secure_val == make_secure_val(val):
        return val

def make_salt(length = 5):
    return ''.join(random.choice(letters) for x in xrange(length))

def make_pw_hash(name, pw, salt = None):
    if not salt:
        salt = make_salt()
    h = hashlib.sha256(name + pw + salt).hexdigest()
    return '%s,%s' % (salt, h)

def valid_pw(name, password, h):
    salt = h.split(',')[0]
    return h == make_pw_hash(name, password, salt)

class User(db.Model):
    name = db.StringProperty(required = True)
    pw_hash = db.StringProperty(required = True)
    email = db.StringProperty()

    @classmethod
    def register(cls, name, pw, email = None):
        pw_hash = make_pw_hash(name, pw)
        return User(parent = users_key(),
                    name = name,
                    pw_hash = pw_hash,
                    email = email)

    @classmethod
    def login(cls, name, pw):
        u = cls.by_name(name)
        if u and valid_pw(name, pw, u.pw_hash):
            return u


class BlogHandler(webapp2.RequestHandler):

    def set_secure_cookie(self, name, val):
        cookie_val = make_secure_val(val)
        self.response.headers.add_header(
            'Set-Cookie',
            '%s=%s; Path=/' % (name, cookie_val))

    def read_secure_cookie(self, name):
        cookie_val = self.request.cookies.get(name)
        return cookie_val and check_secure_val(cookie_val)

    def login(self, user):
        self.set_secure_cookie('user_id', str(user.key().id()))

    def logout(self):
        self.response.headers.add_header('Set-Cookie', 'user_id=; Path=/')

    def initialize(self, *a, **kw):
        webapp2.RequestHandler.initialize(self, *a, **kw)
        uid = self.read_secure_cookie('user_id')
        self.user = uid and User.by_id(int(uid))

让我们仔细检查一下我们这里有什么。

  1. 函数make_secure_val产生如下内容: 'keyword|6f21001bbf8bfbd8c04b9d537df1e314'
  2. 函数make_secure_val用于函数set_secure_cookie。因此,它用于登录。
  3. 函数 make_secure_val 使用密钥,但不使用盐。
  4. 函数make_pw_hash(name, pw, salt = None) 生成如下内容:'some_salt,c984cade696390b71ff914293c53767502ea542bfd0e7240a051e7ead2c60077'
  5. 函数make_pw_hash 准备一个哈希密码,该密码将保存在数据库中。用户在表单中输入用户名和密码,然后表单中的数据成为此 make_pw_hash 函数的参数。
  6. 函数 make_pw_hash 使用盐但不使用密钥。
  7. 函数 make_pw_hash 由于某些原因将用户名混入哈希。

好吧,我在这里什么都不懂。我会说这一切都不安全。

问题:

  1. 为什么老师做饼干不用盐?

  2. 他为什么不用秘钥做hash把密码存入数据库?

  3. 为什么他混合了用户名来做哈希来把密码存入数据库?

  4. 我们可以看到两种生成哈希的方法:一种用于 cookie,另一种用于数据库。他们为什么分开?统一起来不是更实用:总是使用密钥和盐。更少的代码。并且安全性好。但是他分开了。为什么?

顺便说一下The teacher's solution

最重要的第一点:这种存储密码的方法不安全,不应使用或教授。问题是,SHA-256 的单次传递速度太快 (3 Giga SHA-256 per second),因此很容易被暴力破解。我们需要一个在 BCrypt、PBKDF2 或 SCrypt 中可用的成本因素。

对您的问题的一些回答:

  1. 加盐弱密码和强令牌是两种不同的东西。 short/weak 密码需要加盐和密钥拉伸,但对于强令牌可以省略。这要求参数 val 是一个强随机标记(最少 20 个字符 a..z、A..Z、0..9)。这不能通过显示的代码来判断,但应该是 set_secure_cookie().
  2. 文档的一部分
  3. 可以将服务器端机密添加到密码哈希中,但重要的是要了解您要缓解什么以及如何做。如果您有兴趣,请查看我的 tutorial 关于安全密码散列的内容。
  4. 混淆用户名不是散列密码的常用方法,它不会提高安全性但会阻止更改用户名。
  5. 见第一点。