【发布时间】:2020-05-24 11:53:02
【问题描述】:
我一直试图了解对称加密的工作原理以及如何将其集成到我的 CLI 应用程序中,但我遇到了一些问题,我将在下面描述。
我的用例如下:
我有一个 CLI 应用程序 (
SQLAlchemy+click+Python 3.8),它将是一个非常简单的密码管理器(个人使用)。启动时,我想询问用户主密码,以便他能够从数据库中检索任何信息。如果用户还没有主密码,我会要求他创建一个。我希望使用相同的主密钥对所有数据进行加密。
为了做到以上所有,我认为对称加密是最合适的,我想到了Fernet,所以我开始写一些代码:
import base64
from cryptography.fernet import Fernet, InvalidToken
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
def generate_key_derivation(salt, master_password):
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
backend=default_backend()
)
key = base64.urlsafe_b64encode(kdf.derive(master_password.encode()))
return key
def encrypt(key, value_to_encrypt):
f = Fernet(key)
encrypted_key = f.encrypt(value_to_encrypt.encode())
return encrypted_key
def decrypt(key, encrypted_key):
f = Fernet(key)
try:
return f.decrypt(encrypted_key)
except InvalidToken:
return b''
现在,我有点想从文档中理解这一点:
在此方案中,盐必须存储在可检索的位置 以便将来从密码中派生相同的密钥。
在我看来,这意味着:将盐存储在数据库中,并在用户每次尝试使用应用程序时使用它。然后,运行用户通过密钥派生函数插入的主密码并检查它是否匹配......密钥? 但我没有初始密钥,因为我第一次没有将它与盐一起存储。而且如果我要保存,难道没有人可以随意使用它来加密和解密数据吗?
防止上述情况的常用解决方案是什么?
这是一个使用click的小型POC:
import os
import click
from models import MasterPasswordModel
@click.group(help="Simple CLI Password Manager for personal use")
@click.pass_context
def main(ctx):
# if the user hasn't stored any master password yet,
# create a new one
if MasterPasswordModel.is_empty():
# ask user for a new master password
master_password = click.prompt(
'Please enter your new master password: ',
hide_input=True
)
# generate the salt
salt = os.urandom(16)
# generate key_derivation
# this isn't stored because if it does anyone would be able
# to access any data
key = generate_key_derivation(salt, master_password)
# store the salt to the DB
MasterPasswordModel.create(salt)
# if the user stored a master password, check if it's valid and
# allow him to do other actions
else:
# ask user for existing master password
master_password = click.prompt(
'Please enter your new master password: ',
hide_input=True
)
# get existing master password salt from DB
salt = MasterPasswordModel.get_salt()
# generate key_derivation
key = generate_key_derivation(salt, master_password)
# At this point I don't know how to check whether the `key` is
# valid or not since I don't have anything to check it against.
# what am I missing?
我希望所有这些都有意义。作为 TL;DR,我认为问题是:如何安全地存储密钥,以便检索它以进行进一步检查?或者这就是事情应该怎么做?我错过了什么?我确定我误解了一些事情:)
LE:正如其中一个 cmets 中所指定的,看起来我可能有一个解决方案,但我仍然在这个过程中的某个地方卡住了。在this answer 中指定:
如果您还没有这样做,我也强烈建议您不要 直接使用用户提供的密钥,而是先传递它 通过故意缓慢的密钥派生函数,例如 PBKDF2, bcrypt 或 scrypt。你应该先这样做,在尝试之前 验证密钥的正确性,并立即丢弃 用户提供的原始密钥,并将派生密钥用于所有内容 (验证和实际加密/解密)。
所以,让我们一步一步来举例:
1) 我第一次被要求输入主密码。它在 DB 中不存在,所以很明显,我必须创建并存储它。
2) 除了新生成的 salt,我还必须保存提供的主密码的哈希值(例如,我将使用 SHA-256)。
3) 我现在有一个包含盐和散列主密码的记录,因此我可以继续使用该应用程序。我现在想在 DB 中创建一条新记录,据说它会使用我的 key 进行加密。
问题是……什么键?如果我要应用上面写的内容,我必须使用我的 generate_key_derivation() 函数,使用来自 DB 的盐和散列主密码,并将其用于加密/解密。但是,如果我这样做,任何人都无法获取存储在 DB 中的 hash_key,并使用相同的generate_key_derivation 做任何他想做的事吗?
那么,我错过了什么?
【问题讨论】:
-
为什么要存储密钥?给定相同的主密码和相同的盐,您可以派生相同的密钥。所以你只需要储存盐。
-
@PresidentJamesK.Polk 我知道。但是,如果您没有密钥,如何验证主密码是正确的呢?
-
啊,我明白了,这是个好问题。我可以想到一些特别的想法,比如从 KDF 生成 32 个额外的字节并将 16 个字节的一半异或并存储起来。但是您应该使用正确分析的方法,而我不知道任何方法。
-
在加密堆栈交换上有一个很好的答案:crypto.stackexchange.com/questions/1507/…
-
@rfkortekaas 如果我做对了(接受答案中的前一种方法),我应该添加一个散列函数(可能是 SHA-512),它基本上会散列用户将输入的
master_password并将该哈希存储在盐旁边,对吗?
标签: python encryption sqlalchemy encryption-symmetric