2
\$\begingroup\$

Since I only just learnt about this, I wanted some advice on better practices I can use when it comes to cryptography. Still learning python and hoping I can get this 'hashing' and 'salting' thing in the bag. I plan on using this for a personal password manager I am making.

So basically what I did was, hash a password with a salt included using the pypasswords library. Using the key made from this, I derive another key. I then to make the hacker go through more steps, encrypt the derived key and salt using the cryptography.fernet library. I then save the encrypted derived key, encrypted salt, and the key for decryption in a .json file. Reason being I plan on saving other things to said file to.

from pypasswords import *
from cryptography.fernet import Fernet
import json


def base_generator(password: str):

    """
    Generates key and salt with password and then rehashes to make a derived key.
    This derived key is then saved in a dictionary with the salt for further security
    This one must get the password, if they want to get in.

    dictionary = {'main': {'derived_key': derived_key, 'salt': salt}}
    """

    key, salt = hash_it(password, salting=True)
    derived_key = hash_it(key)
    json_save = {
        'main': {
            'derived_key': derived_key,
            'salt': salt
        }
    }

    return json_save


def re_hash(json_save: dict):

    """
    Re-encrypts the values with the key for further security if any at all as the key is still saved
    in the dictionary.

    dictionary = {'main': {'derived_key': derived_key, 'salt': salt, 'key': key}}
    """

    key = Fernet.generate_key()
    json_save['main']['derived_key'] = Fernet(key).encrypt(json_save['main']['derived_key'].encode()).decode()
    json_save['main']['salt'] = Fernet(key).encrypt(json_save['main']['salt'].encode()).decode()
    json_save['main']['key'] = key.decode()

    return json_save


def password_hasher(password: str):

    """
    Combination of 'base_generator' and 're_hash'
    """

    json_save = base_generator(password)
    json_save = re_hash(json_save)

    return json_save


def password_checker(json_save, password):

    """
    Basically undoes all the hashing and checks if password and salt equal to derived key
    """

    # Converting rehashed values back to originals
    key = json_save['main']['key'].encode()
    salt = Fernet(key).decrypt(json_save['main']['salt'].encode()).decode()
    derived_key = Fernet(key).decrypt(json_save['main']['derived_key'].encode()).decode()

    # Preparing for password confirmation
    password_key, salt = hash_it(password, salting=True, static_salt=salt)

    # Checking for password_confirmation with derived_key
    return match_it(password_key, derived_key)


def json_file(command, json_data):

    if command in ['dump', 'load']:
        if command == 'dump':
            with open('data.json', 'w') as data_file:
                json.dump(json_data, data_file, indent=4)

        if command == 'load':
            with open('data.json', 'r') as data_file:
                json_save = json.load(data_file)

            return json_save

    else:
        print('Invalid Command. Try "load", "dump"')


def main():
    password1 = input('Password: ')
    password2 = input('What was password? ')
    json_save = password_hasher(password1)

    json_file('dump', json_save)
    json_save = json_file('load', json_save)

    print(password_checker(json_save, password2))
    print(password1, password2, json_save)


if __name__ == '__main__':
    main()

Please bear in mind that I'm still just beginning and don't fully understand cryptography. With that in mind, I look forward to the advice and criticism.

\$\endgroup\$
0

1 Answer 1

2
\$\begingroup\$

I wanted some advice on better practices I can use when it comes to cryptography

Cryptographic advice items number 0 through 999 are: don't do this yourself. Don't write the crypto yourself, and unless you have a very strange, specific and inflexible requirement, don't write the utilities and wrappers yourself, either. It's especially true for beginners, but it's equally true for the general population of programmers who haven't been in the security & crypto trenches for years. There are entire categories of vulnerability that most people don't even think about, like timing attacks. It takes years of things going wrong and being fixed ("high exposure") for a package or program to be considered secure. As a starting point, it helps to read the source code of established programs.

Some of the absolute minimum security best-practices are not present here. You display the user's password in plain-text; you should use getpass instead. After that, there is a long litany of other things to be considered: where specifically is your data.json stored? With what file permissions and what user ownership? Do you need to add padding to prevent size attacks? Are there minimum length and entropy requirements (not character class requirements) for the password? And so on.

I plan on saving other things to said file to.

is almost certainly a bad idea; but if you persist with it, your current code will not work because you unconditionally steamroll anything that had been in the file beforehand.

As to the code itself:

Avoid importing *. You only need two symbols from pypasswords.

Don't pass around json_save, as that's a serialization detail. Only pass around the variables you care about.

Writing json_file to accept one of two command strings is less helpful than just writing two separate functions.

A first pass is:

from getpass import getpass
from pathlib import Path
from typing import Any

from pypasswords import hash_it, match_it
from cryptography.fernet import Fernet
import json


DATA_PATH = Path('data.json')


def base_generator(password: str) -> tuple[
    str,  # derived key
    str,  # salt
]:
    """
    Generates key and salt with password and then rehashes to make a derived key.
    This derived key is then saved with the salt for further security
    """

    key, salt = hash_it(password, salting=True)
    derived_key = hash_it(key)
    return derived_key, salt


def re_hash(derived_key: str, salt: str) -> tuple[
    str,  # derived key
    str,  # salt
    str,  # key
]:
    """
    Re-encrypts the values with the key for further security if any at all as the key is still saved
    in the dictionary.
    """

    key = Fernet.generate_key()
    derived_key = Fernet(key).encrypt(derived_key.encode()).decode()
    salt = Fernet(key).encrypt(salt.encode()).decode()
    key = key.decode()

    return derived_key, salt, key


def password_hasher(password: str) -> tuple[
    str,  # derived key
    str,  # salt
    str,  # key
]:
    """
    Combination of 'base_generator' and 're_hash'
    """

    derived_key, salt = base_generator(password)
    derived_key, salt, key = re_hash(derived_key, salt)

    return derived_key, salt, key


def password_checker(derived_key: str, salt: str, key: str, password: str) -> bool:
    """
    Basically undoes all the hashing and checks if password and salt equal to derived key
    """

    # Converting rehashed values back to originals
    key = key.encode()
    salt = Fernet(key).decrypt(salt.encode()).decode()
    derived_key = Fernet(key).decrypt(derived_key.encode()).decode()

    # Preparing for password confirmation
    password_key, _ = hash_it(password, salting=True, static_salt=salt)

    # Checking for password_confirmation with derived_key
    return match_it(password_key, derived_key)


def dump_json(derived_key: str, salt: str, key: str) -> None:
    if DATA_PATH.exists():
        with DATA_PATH.open() as f:
            data: dict[str, Any] = json.load(f)
    else:
        data = {}

    main = data.setdefault('main', {})
    main.update(derived_key=derived_key, salt=salt, key=key)

    with DATA_PATH.open('w') as f:
        json.dump(data, f)


def load_json() -> tuple[
    str,  # derived key
    str,  # salt
    str,  # key
]:
    with DATA_PATH.open() as f:
        main = json.load(f)['main']
    return main['derived_key'], main['salt'], main['key']


def main() -> None:
    print('! Do not use this for real credentials. You have been warned. !')

    password_1 = getpass('Password: ')
    derived_key, salt, key = password_hasher(password_1)
    dump_json(derived_key, salt, key)
    derived_key, salt, key = load_json()

    password_2 = getpass('What was password? ')
    if password_checker(derived_key, salt, key, password_2):
        print('Password check succeeded')
    else:
        print('Password check failed')


if __name__ == '__main__':
    main()
\$\endgroup\$
3
  • 2
    \$\begingroup\$ Thank you so much. I didn't know I was wrong by so much. I will NOT be using my code to store my passwords lol. I think I will be staying away from this for a while. \$\endgroup\$
    – Beginner
    Jul 11, 2023 at 1:08
  • \$\begingroup\$ Well. It's good to have interest, and I hope that I haven't dissuaded you from learning security topics. It's important and interesting in part because it's so difficult. \$\endgroup\$
    – Reinderien
    Jul 11, 2023 at 2:38
  • 1
    \$\begingroup\$ No not at all, I will try to pick this up again, further into my coding career \$\endgroup\$
    – Beginner
    Jul 12, 2023 at 5:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.