0

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:

  1. User provides a human-readable password.
  2. The system generates a random salt.
  3. Using PBKDF2, the system derives a hash (key) from the password and the salt.
  4. The hash is concatenated with the file content.
  5. The combined data is encrypted using Fernet.
  6. Finally, the plaintext salt and the encrypted data are placed together in a file.

Decryption and Password Verification:

  1. User provides the password and the encrypted file.
  2. The system regenerates the hash (key) using the password and the stored salt in the encrypted file.
  3. It decrypts the file content.
  4. 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")

5
  • 4
    What are you using as the key? The hash+message? Aren't you also going to have to store the PBKDF2 settings? This looks like you are trying to reinvent a wheel.
    – schroeder
    Oct 7, 2023 at 10:42
  • There are standard ways of checking whether the correct key is used for decryption. See crypto.stackexchange.com/questions/44311/… and stackoverflow.com/questions/50507211/key-verification-in-aes for some interesting reading on this subject.
    – mti2935
    Oct 7, 2023 at 13:19
  • @schroeder, I've written some code and attempted to explain the concept behind it. I'm looking for feedback on whether this approach is appropriate.
    – Patrickliu
    Oct 8, 2023 at 0:48
  • How is this not storing the hash? Oct 10, 2023 at 4:18
  • @RoyceWilliams, I am trying not to store the hash in local, so i store the hash in the encrypted file.
    – Patrickliu
    Oct 10, 2023 at 7:48

1 Answer 1

1

This is safe, to within the limits of your PHF/KDF[1] and your implementation of the Fernet construction[2]. You're basically taking the password, hashing it (to produce the Fernet key), and then hashing that again (with the non-authentication-tag parts of the Fernet "token") to produce the Fernet authentication tag - which is the last 32 bytes of the "Fernet token" - and comparing that to the one in the token. This is safe (far more so than merely checking whether "the decrypted file still contains garbled characters", which is impossible to do reliably) and can even be reasonably fast.

Since the Fernet authentication tag is generated over the "token" (that is to say, over the ciphertext), you can (and should) verify the authentication tag before doing any actual decryption. This is good for your users (it means you can detect a wrong password relatively quickly, compared to the time needed to decrypt the file), and good for security (you don't want to risk returning maliciously-modified data, or accidentally accepting a wrong password, so better to check and fail early) so hopefully that's how your Fernet implementation performs decryption.

[1] Password Hashing Functions (and Key Derivation Functions, two mostly-overlapping categories) vary widely in security, both intrinsically and in tunable security parameters. PBKDF2 is among the weakest of the PHFs still in widespread use, as it completely lacks "memory hardness" and thus can be trivially parallelized on cheap hardware such as commodity GPUs. Bcrypt - also quite old - has a small but meaningful memory cost, but the cost is fixed and insufficient to prevent parallelization on modern hardware. Scrypt and especially the argon2 family are more modern functions, with - among other things - tunable memory costs. While those functions are designed specifically as PHFs, I believe they are suitable as KDFs as well.

[2] The Fernet construction - an authenticated encryption scheme (which always includes a version tag and timestamp - and nothing else - as associated data) - is built on AES-128-CBC with random 128-bit IVs plus PKCS #7 padding (for confidentiality) and HMAC-SHA2-256 (over the ciphertext, IV, version tag, and timestamp) for integrity, using separate keys for each. These are perfectly reasonable primitives, security-wise, provided that the HMAC is checked before decryption is attempted (to avoid risks such as padding oracle attacks). Quantum computers may be able to break AES-128 at some point, which is one of a few reasons why AES-256 is generally preferred now, but that point is probably decades off, and for conventional hardware it's perhaps centuries off. However, all of this is moot if the implementation isn't good enough! Getting cryptography right is hard. All sorts of flaws - from simple stuff like using the wrong source for your random numbers to subtle flaws like timing or power side-channels on the cryptography itself that can reveal the key - can drastically weaken cryptography. Make sure you're using a good implementation, though, and this scheme should work.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .