1

I'm currently in the process of building a password manager as a project to get a better understanding of PostgreSQL, alongside learning how to handle data between Python and a database.


Here's a little about the logic of my password manager.

  • If you're a new user, the program will greet you with a prompt awaiting a master password to be set, to access your password vault. Once set, the master password is encrypted using the hashlib Python module as you can see below in the code snippet. A key and a salt is used to encrypt the password, and both are stored as binary values in the database. These values are sent to the databse using the psycopg2 Python module.
  • If you're an existing user, the program will greet you with a promt awaiting the already-set master password so you can access with your password vault. The program will fetch the key and salt from the database, use the salt to create a new key and see if the key and new key values match. If they do, access granted, otherwise access denied to the vault.

However, I have encountered the following problem: the binary data (key and salt) that is stored in the database is not the same as the binary data (key and salt) in Python. Below you will find my code:

import psycopg2
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
import sys
import os
import hashlib

# This connects to PSQL
connection = psycopg2.connect(user="postgres",
                              password="abc",
                              host="localhost",
                              port="5432",
                              dbname="passwordmanager")

connection.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
cur = connection.cursor()

salt = b""
key = b""
new_key = b""
stored_salt = b""


def welcome_n():  # This welcomes a new user without a master password set
    user_input = input("Welcome! You need to set a master password"
                       " before you can start storing passwords - continue?\n    y/n: ")
    valid = False
    while not valid:
        if user_input == "y":
            password_input = input("Please choose your password:\n")
            global salt
            global key

            salt = os.urandom(32)
            key = hashlib.pbkdf2_hmac(
                "sha256",
                password_input.encode("utf-8"),
                salt,
                100000
            )
            # PSQL commands to store salt and key
            psycopg2.Binary(key)
            psycopg2.Binary(salt)
            cur.execute("INSERT INTO master (key, salt) VALUES (%s, %s)",
                        (key, salt))
            valid = True
            print(key)  # b'\xfe\xfdO{\xd6?\xa1\x8d(\xbb\xb3r\x8a\xbc\xd6&t\x11[\x06\x110`\xb3\xfa\x91\xee\xc7x\x14\xddR'
            key = b""

        elif user_input == "n":
            print("Quiting program.")
            sys.exit()
        else:
            user_input = input("Invalid input. Please choose either 'y' or 'n'.\n    y/n: ")


def welcome_o():  # This welcomes an existing user with a master password set already
    password_to_check = input("Welcome! Enter your password to access the password safe:\n")

    # Encrypting the password given
    global salt
    global key
    global new_key
    global stored_salt

    cur.execute("SELECT key FROM master;")
    key = cur.fetchone()  # The key variable now has the fetched binary key from database
    cur.execute("SELECT salt FROM master;")
    stored_salt = cur.fetchone()  # The stored_salt variable now has the fetched binary salt from database

    new_key = hashlib.pbkdf2_hmac("sha256", password_to_check.encode("utf-8"), b"stored_salt", 10000)
    # A new key has been generated using the fetched salt

    if new_key == key:
        print("Password is correct")
    else:
        print("Incorrect Password")

    # Checking password similarities
    print(key)  # (<memory at 0x03B96388>,)
    print(new_key)  # b"k\r\xd8\xcfU\x05x\xfc'9\xaaC\x1fp~*9av6k^\xeeec\xef\xc5\xe3\xf1^\x883"


welcome_n()
welcome_o()

As I aforementioned, the binary data that is stored in the database is not the same as the binary data in Python. We can see that in the key;


Python binary = b'\xfe\xfdO{\xd6?\xa1\x8d(\xbb\xb3r\x8a\xbc\xd6&t\x11[\x06\x110`\xb3\xfa\x91\xee\xc7x\x14\xddR'

vs

PSQL binary = \xfefd4f7bd63fa18d28bbb3728abcd62674115b06113060b3fa91eec77814dd52


Another problem I'm having is that, when I try printing on screen the key, I get (<memory at 0x03B96388>,) shown above, while the new key displays as b"k\r\xd8\xcfU\x05x\xfc'9\xaaC\x1fp~*9av6k^\xeeec\xef\xc5\xe3\xf1^\x883" . So far, my guesses are that I'm not inputting the data correctly into the database and not extracting it correctly, meaning that the binary data is deformed. I would also like to mention that the table holding the master password binary data has the key column and salt column data types set to BYTEA. You can find the table schema below:

Column |  Type  | Collation | Nullable |               Default              | Storage | Stats target | Description
-------+--------+-----------+----------+------------------------------------+---------+--------------+-------------
id     | bigint |           | not null | nextval('master_id_seq'::regclass) |  plain  |              |
key    | bytea  |           | not null |                                    | extended|              |
salt   | bytea  |           | not null |                                    | extended|              |

Any feedback in any form is much appreciated, I'm new to programming and I'm looking to improve and learn new things!

5
  • Can you show us the schema of the relevant table, please? Commented Oct 26, 2020 at 0:32
  • You're running pbkdf2_hmac with different iterations. They should share a single constant. Commented Oct 26, 2020 at 0:51
  • @Schwern Here's the schema. Commented Oct 27, 2020 at 21:43
  • Ok, good, you're using bytea. If you could edit that into your question please. Avoid screenshots, paste the text in. Commented Oct 27, 2020 at 22:38
  • I've updated the answer. With those fixes the code now works. Consider a tackling binary data after you've got the basics of Python and SQL down. Commented Oct 27, 2020 at 23:00

1 Answer 1

3

When you read a bytea you will get back a memoryview object. This needs to be converted to bytes.

key = bytes(key)

See How to read and insert bytea columns using psycopg2? for more.

But there's other problems with your code. Given that you're struggling with Python and SQL, I would suggest a simpler exercise that does not involve binary data.


            psycopg2.Binary(key)
            psycopg2.Binary(salt)
            cur.execute("INSERT INTO master (key, salt) VALUES (%s, %s)",
                        (key, salt))

Your calls to psycopg2.Binary here do nothing.

psycopg2.Binary(key) does not alter key. It returns a psycopg2.extensions.Binary representation of the key. You're doing the conversions, throwing them out, and inserting the original unconverted value.

Instead, you need to insert the return value from psycopg2.Binary.

            cur.execute("INSERT INTO master (key, salt) VALUES (%s, %s)",
                        (psycopg2.Binary(key), psycopg2.Binary(salt)))

But it turns out you don't need it at all. You can pass in key and value and psycopg2 will figure it out.

            cur.execute("INSERT INTO master (key, salt) VALUES (%s, %s)",
                        (key, salt))

See Binary Adaptation in the psycopg2 docs.


fetchone() does not return one column, it returns one row as a tuple. key = cur.fetchone() returns a tuple with the key as the first item. key == new_key will never work.

Instead, fetch the row and pull it apart. This is correct and also more efficient than making two queries for the same row.

cur.execute("SELECT key, salt FROM master;")
(key, stored_salt) = cur.fetchone()

Your second call to hashlib.pbkdf2_hmac is incorrect. It's using a different number of iterations, and it's using the literal string "stored_salt" as the salt, not the contents of the stored_salt variable.

Here is the correct call.

    new_key = hashlib.pbkdf2_hmac(
        "sha256",
        password_to_check.encode("utf-8"),
        stored_salt,
        100000
    )

To avoid this, make a single function for calculating keys.

def make_key(password, salt):
    return hashlib.pbkdf2_hmac(
        "sha256",
        password.encode("utf-8"),
        salt,
        100000
    )

Your variables do not need to be global. This probably won't break anything, but it's bad practice.


Because your select queries lack a where clause, there's no guarantee the row you select is the same one you inserted earlier. You need something to connect them, like a username.

Sign up to request clarification or add additional context in comments.

1 Comment

I was considering at one point programming this password vault without binary to detect other viable options but I most definitely will look deeper into the topic of binary. Much appreciate everything!

Your Answer

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.