Python 中使用 Fernet 的对称加密 - 主密码用例
Symmetric encryption using Fernet in Python - Master password use case
我一直在尝试了解对称加密的工作原理以及如何将其集成到我的 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''
现在,我有点试图从文档中理解这一点:
In this scheme, the salt has to be stored in a retrievable location in
order to derive the same key from the password in the future.
这在我看来意味着:将盐存储在数据库中,并在用户每次尝试使用该应用程序时使用它。然后,运行 用户通过密钥派生函数插入的主密码,并检查它是否匹配……密钥? 但是我没有初始密钥,因为我第一次没有将它与盐一起存储。而且如果我保存下来,岂不是任何人都可以随意使用它来加密和解密数据吗?
防止上述情况的常用解决方案是什么?
这是一个使用 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:正如其中一条评论中所指出的那样,看起来我可能有一个解决方案,但我仍然被困在这个过程中的某个地方。在 this answer 中指定:
If you're not doing this already, I'd also strongly recommend not
using the user-supplied key directly, but instead first passing it
through a deliberately slow key derivation function such as PBKDF2,
bcrypt or scrypt. You should do this first, before even trying to
verify the correctness of the key, and immediately discard the
original user-supplied key and use the derived key for everything
(both verification and actual en/decryption).
所以,让我们一步一步地举个例子:
1) 我第一次被要求输入主密码。它在数据库中不存在,所以很明显,我必须创建并存储它。
2) 除了新生成的盐,我还必须保存提供的主密码的哈希值(为了举例,我将使用 SHA-256)。
3) 我现在有一个包含 salt 和散列主密码的记录,因此我可以继续使用该应用程序。我现在想在数据库中创建一条新记录,据说它将使用我的 key.
进行加密
问题是……什么键?如果我要应用上面写的内容,我必须使用我的 generate_key_derivation()
函数,使用来自 DB 的盐和散列主密码并将其用于 encryption/decryption。但是,如果我这样做,难道任何人都无法只使用存储在数据库中的 hash_key,并使用相同的 generate_key_derivation
来做他想做的事吗?
那么,我错过了什么?
我不是加密专家,但我认为这个想法是像这样存储派生密钥的盐和散列:
- 第一次获取主密码
- 生成盐
- 使用 salt 和主密码导出新密钥
- 放弃主密码
- 散列派生密钥
- 将盐和派生密钥存储在数据库中
- 使用派生密钥加密存储的密码
稍后使用 salt 和 hash 来验证派生密钥的真实性,如下所示:
- 获取主密码
- 从数据库中获取盐和散列
- 使用盐和主密码导出密钥
- 放弃主密码
- 散列派生密钥
- 通过查看散列是否与数据库中的散列匹配来验证派生密钥
- 如果不匹配,退出
- 否则,使用派生密钥解密其他密码。
我一直在尝试了解对称加密的工作原理以及如何将其集成到我的 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''
现在,我有点试图从文档中理解这一点:
In this scheme, the salt has to be stored in a retrievable location in order to derive the same key from the password in the future.
这在我看来意味着:将盐存储在数据库中,并在用户每次尝试使用该应用程序时使用它。然后,运行 用户通过密钥派生函数插入的主密码,并检查它是否匹配……密钥? 但是我没有初始密钥,因为我第一次没有将它与盐一起存储。而且如果我保存下来,岂不是任何人都可以随意使用它来加密和解密数据吗?
防止上述情况的常用解决方案是什么?
这是一个使用 click
:
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:正如其中一条评论中所指出的那样,看起来我可能有一个解决方案,但我仍然被困在这个过程中的某个地方。在 this answer 中指定:
If you're not doing this already, I'd also strongly recommend not using the user-supplied key directly, but instead first passing it through a deliberately slow key derivation function such as PBKDF2, bcrypt or scrypt. You should do this first, before even trying to verify the correctness of the key, and immediately discard the original user-supplied key and use the derived key for everything (both verification and actual en/decryption).
所以,让我们一步一步地举个例子:
1) 我第一次被要求输入主密码。它在数据库中不存在,所以很明显,我必须创建并存储它。
2) 除了新生成的盐,我还必须保存提供的主密码的哈希值(为了举例,我将使用 SHA-256)。
3) 我现在有一个包含 salt 和散列主密码的记录,因此我可以继续使用该应用程序。我现在想在数据库中创建一条新记录,据说它将使用我的 key.
进行加密问题是……什么键?如果我要应用上面写的内容,我必须使用我的 generate_key_derivation()
函数,使用来自 DB 的盐和散列主密码并将其用于 encryption/decryption。但是,如果我这样做,难道任何人都无法只使用存储在数据库中的 hash_key,并使用相同的 generate_key_derivation
来做他想做的事吗?
那么,我错过了什么?
我不是加密专家,但我认为这个想法是像这样存储派生密钥的盐和散列:
- 第一次获取主密码
- 生成盐
- 使用 salt 和主密码导出新密钥
- 放弃主密码
- 散列派生密钥
- 将盐和派生密钥存储在数据库中
- 使用派生密钥加密存储的密码
稍后使用 salt 和 hash 来验证派生密钥的真实性,如下所示:
- 获取主密码
- 从数据库中获取盐和散列
- 使用盐和主密码导出密钥
- 放弃主密码
- 散列派生密钥
- 通过查看散列是否与数据库中的散列匹配来验证派生密钥
- 如果不匹配,退出
- 否则,使用派生密钥解密其他密码。