Question:
I am working on a password-based file encryption and decryption system in Python using the PBKDF2 key derivation function and Fernet encryption. I have a specific requirement: I want to verify a user's password without storing the hash permanently. Here's my approach, and I'd like to know if it's safe or if there are better alternatives:
Encryption Process:
- User provides a human-readable password.
- The system generates a random salt.
- Using PBKDF2, the system derives a hash (key) from the password and the salt.
- The hash is concatenated with the file content.
- The combined data is encrypted using Fernet.
- Finally, the plaintext salt and the encrypted data are placed together in a file.
Decryption and Password Verification:
- User provides the password and the encrypted file.
- The system regenerates the hash (key) using the password and the stored salt in the encrypted file.
- It decrypts the file content.
- Finally, it checks if the stored hash (from decryption) matches the regenerated hash. If they match, the password is verified. If not, and nothing is found (the decrypted file still contains garbled characters), it will raise a wrong password error.
I'd appreciate any insights, suggestions, or recommendations regarding the security and feasibility of this approach. If there are alternative methods or best practices to achieve password verification without permanently storing the hash, please share your expertise.
Edited:
This code is an implementation of my idea.
import base64
import os
import pickle
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
# Define the length of the salt
SALT_LEN = 64
# Define the password for key derivation
password = "A password"
# Function to generate a cryptographic key from a password and salt
def generate_key_from_password(salt: bytes) -> bytes:
kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), salt=salt, iterations=100000, length=32)
return base64.urlsafe_b64encode(kdf.derive(password))
# Function to encrypt and pickle an object to a file
def encrypt_pickle(filepath: str, obj):
pickled = pickle.dumps(obj)
salt = os.urandom(SALT_LEN)
key = generate_key_from_password(salt)
# Encrypt the pickled data and append the key to it
encrypted_pickle = Fernet(key).encrypt(pickled + key)
# Write the salt and encrypted data to the file
with open(filepath, "wb") as file:
file.write(salt + encrypted_pickle)
# Function to decrypt a pickled object from a file
def decrypt_pickle(filepath: str):
with open(filepath, "rb") as file:
data = file.read()
salt, encrypted_pickle = data[:SALT_LEN], data[SALT_LEN:]
# Generate a cryptographic key from the password and salt
key = generate_key_from_password(salt)
try:
# Try to decrypt the encrypted data
decrypted = Fernet(key).decrypt(encrypted_pickle)
# If success verify the key
decrypted_pickle, decrypted_key = decrypted[:-44], decrypted[-44:]
if decrypted_key == key:
success = True
except:
success = False
# If the decryption was successful, return the unpickled object;
# otherwise, raise an exception
if success:
return pickle.loads(decrypted_pickle)
else:
raise Exception("Wrong password")
hash+message? Aren't you also going to have to store the PBKDF2 settings? This looks like you are trying to reinvent a wheel.