保护敏感数据的编码策略

10

一个web应用程序包含用户的敏感数据,既不应该是web应用程序的操作者也不应该是托管提供商能够查看这些数据。因此,我希望将这些数据加密后通过用户的登录密码存储在数据库中。

dataInDB = encrypt (rawData, user password) 

使用这种策略无法实现通常用于密码恢复的用例:由于 Web 应用程序通常仅存储密码的哈希值,因此应用程序无法将旧的、遗忘的密码发送给用户。而且通过分配新的、偶然的密码,数据库中的加密数据将不再可读。

还有其他解决方案吗?


1
+1:好问题。处境真糟糕... - Adam Paynter
1个回答

7

一种可能的解决方案(我不对任何破坏负责):

在加密敏感数据时,不要使用用户的密码作为密钥。相反,应该从用户的密码中派生密钥(最好使用标准算法如PBKDF2)。以防用户忘记他们的密码,您可以保留此派生密钥的副本(使用从用户的答案派生的另一个密钥进行加密)。如果用户忘记了他们的密码,他们可以回答他们的安全问题。只有正确的答案才能解密原始密码密钥(而不是原始密码)。这使您有机会重新加密敏感信息。

我将使用(类似Python的)伪代码进行演示,但首先让我们看一下可能的用户表。暂时不要被列所困扰,它们很快就会变得清晰...

CREATE TABLE USERS
(
    user_name               VARCHAR,

    -- ... lots of other, useful columns ...

    password_key_iterations NUMBER,
    password_key_salt       BINARY,
    password_key_iv         BINARY,
    encrypted_password_key  BINARY,
    question                VARCHAR,
    answer_key_iterations   NUMBER,
    answer_key_salt         BINARY
)

当需要注册用户时,他们必须提供一个问题和答案:

def register_user(user_name, password, question, answer):
    user = User()

    # The question is simply stored for later use
    user.question = question

    # The password secret key is derived from the user's password
    user.password_key_iterations = generate_random_number(from=1000, to=2000)
    user.password_key_salt = generate_random_salt()
    password_key = derive_key(password, iterations=user.password_key_iterations, salt=user.password_key_salt)

    # The answer secret key is derived from the answer to the user's security question
    user.answer_key_iterations = generate_random_number(from=1000, to=2000)
    user.answer_key_salt = generate_random_salt()
    answer_key = derive_key(answer, iterations=user.answer_key_iterations, salt=user.answer_key_salt)

    # The password secret key is encrypted using the key derived from the answer
    user.password_key_iv = generate_random_iv()
    user.encrypted_password_key = encrypt(password_key, key=answer_key, iv=user.password_key_iv)

    database.insert_user(user)

如果用户忘记了他们的密码,系统仍然需要要求用户回答他们的安全问题。他们的密码无法恢复,但可以从密码中派生出密钥。这使得系统可以使用密码重新加密敏感信息:

def reset_password(user_name, answer, new_password):
    user = database.rerieve_user(user_name)

    answer_key = derive_key(answer, iterations=user.answer_key_iterations, salt=user.answer_key_salt)

    # The answer key decrypts the old password key
    old_password_key = decrypt(user.encrypted_password_key, key=answer_key, iv=user.password_key_iv)
    
    # TODO: Decrypt sensitive data using the old password key

    new_password_key = derive_key(new_password, iterations=user.password_key_iterations, salt=user.password_key_salt)

    # TODO: Re-encrypt sensitive data using the new password key

    user.encrypted_password_key = encrypt(new_password_key, key=user.answer_key, iv=user.password_key_iv)

    database.update_user(user)

当然,这里并没有明确强调的一些通用的加密原则(如密码模式等),实现者需要熟悉这些原则。
希望这能对您有所帮助! :)
Eadwacer的评论提到:
我建议避免直接从密码派生密钥(熵受限且更改密码需要重新加密所有数据)。而是为每个用户创建一个随机密钥,并使用密码加密该密钥。您还将使用从安全问题中派生的密钥加密该密钥。
这是我修改后的解决方案,考虑到他的优秀建议:
CREATE TABLE USERS
(
    user_name                      VARCHAR,

    -- ... lots of other, useful columns ...

    password_key_iterations        NUMBER,
    password_key_salt              BINARY,
    password_encrypted_data_key    BINARY,
    password_encrypted_data_key_iv BINARY,
    question                       VARCHAR,
    answer_key_iterations          NUMBER,
    answer_key_salt                BINARY,
    answer_encrypted_data_key      BINARY,
    answer_encrypted_data_key_iv   BINARY,
)

您需要按照以下方式注册用户:
def register_user(user_name, password, question, answer):
    user = User()

    # The question is simply stored for later use
    user.question = question

    # The randomly-generated data key will ultimately encrypt our sensitive data
    data_key = generate_random_key()

    # The password key is derived from the password
    user.password_key_iterations = generate_random_number(from=1000, to=2000)
    user.password_key_salt = generate_random_salt()
    password_key = derive_key(password, iterations=user.password_key_iterations, salt=user.password_key_salt)

    # The answer key is derived from the answer
    user.answer_key_iterations = generate_random_number(from=1000, to=2000)
    user.answer_key_salt = generate_random_salt()
    answer_key = derive_key(answer, iterations=user.answer_key_iterations, salt=user.answer_key_salt)

    # The data key is encrypted using the password key
    user.password_encrypted_data_key_iv = generate_random_iv()
    user.password_encrypted_data_key = encrypt(data_key, key=password_key, iv=user.password_encrypted_data_key_iv)
    
    # The data key is encrypted using the answer key
    user.answer_encrypted_data_key_iv = generate_random_iv()
    user.answer_encrypted_data_key = encrypt(data_key, key=answer_key, iv=user.answer_encrypted_data_key_iv)

    database.insert_user(user)

现在,重置用户密码的步骤如下所示:
def reset_password(user_name, answer, new_password):
    user = database.rerieve_user(user_name)

    answer_key = derive_key(answer, iterations=user.answer_key_iterations, salt=user.answer_key_salt)

    # The answer key decrypts the data key
    data_key = decrypt(user.answer_encrypted_data_key, key=answer_key, iv=user.answer_encrypted_data_key_iv)
    
    # Instead of re-encrypting all the sensitive data, we simply re-encrypt the password key
    new_password_key = derive_key(new_password, iterations=user.password_key_iterations, salt=user.password_key_salt)

    user.password_encrypted_data_key = encrypt(data_key, key=new_password_key, iv=user.password_encrypted_data_key_iv)

    database.update_user(user)

希望今晚我的头脑依然清晰...

这段内容与IT技术无关。

@Dominik:你说得对,这就是为什么我们将迭代和盐值存储在数据库中的原因。 - Adam Paynter
@Frank:请参考https://dev59.com/_HVC5IYBdhLWcg3wykUt。他们建议使用16字节的随机数据。 - Adam Paynter
1
我建议避免直接从密码派生密钥(熵值有限,更改密码需要重新加密所有数据)。相反,为每个用户创建一个随机密钥,并使用密码来加密密钥。您还可以使用从安全问题派生的密钥来加密密钥。 - Eadwacer
@Eadwacer:感谢你的提示:今天我读了Bruce Schneier的《Secrets & Lies》几章,了解了有关用户密码熵的有趣事实。看起来你的提示可以稍微增加这里给出的解决方案的安全性。但这是因为我喝的红酒还是其他原因:如果用户忘记密码,如何在没有密码的情况下解密密钥(用于解密用户数据)? - Dominik
@Dominik: Eadwacer建议你维护第三个密钥,我们可以称之为数据密钥。这是最终加密敏感数据的密钥。注册用户时,随机生成此数据密钥。然后使用密码密钥对其进行加密,并将结果存储在USERS表的某个字段中。接着再使用答案密钥进行加密,将结果存储在USERS表的另一个字段中。这样,你仍然可以通过答案密钥获取数据密钥。 - Adam Paynter
显示剩余5条评论

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