How to Pythonically Store Sensitive and Encrypted Information in a Database for a Web Application the Right Way – Part 2 

Spread the love

In my previous blog post, How to Pythonically Store Sensitive and Encrypted Information in a Database for a Web Application the Right Way – Part 1, I discussed cryptographic concepts, such as data encryption keys, key encryption keys, and key derivation functions, and I also discussed how to use these concepts to store sensitive information in a database. This blog post is a continuation of the previous one where I will discuss how to fetch the encrypted information from the database and decrypt the information. Therefore, you may want to read the part 1 of this blog post before proceeding with this blog post.

First, let’s review some important cryptographic concepts.

Prerequisite Cryptographic Concepts

Before diving into the code, the following are the prerequisite cryptographic concepts you must be aware of to follow along with this blog post:

Advanced Encryption Standard (AES) 256-bit – AES is a symmetric key block cipher algorithm for encrypting and decrypting data with a single key (Bernstein & Cobb, 2021).

Data Encryption Key (DEK) – The DEK is a cryptographic key used to encrypt and decrypt the data (“Data Encryption Key”, 2014).

Key Encryption Key (KEK) – The KEK is the cryptographic key used to encrypt and decrypt the DEK (“Key-encryption-key (KEK) – glossary”, n.d.).

Key Derivation Function (KDF) – “A KDF is a cryptographic algorithm designed to generate a secure secret key from a single key value” (“What Are Key Derivation Functions?”, 2023). In a Password Based KDF (PBKDF), the KDF takes a password, salt, difficulty level, and key size to generate a secret key.

Cryptographic Salt – A salt in cryptography is a random value used to make the hash of a password unique by appending or prepending the salt to the password before computing the hash (“What is a cryptographic salt?”, n.d.).

Since in the previous blog post, I discussed the steps to store encrypted information in a database like Elasticsearch the right way, I will discuss the steps to retrieve and decrypt the encrypted data stored in a database like Elasticsearch.

How to Securely Retrieve Sensitive Information in a Database like Elasticsearch

Securely retrieving sensitive information in a database can be done in eight steps (“How to encypt sensitive data in database of a web app?”, n.d.):

  1. Retrieve the document or record when the user logs in.
  2. Extract the KEK salt stored in the document retrieved (from step 1) and base 64 decode it.
  3. Regenerate the KEK with the user’s plaintext password from the login and base 64 decoded KEK salt (from step 2).
  4. Retrieve the base 64 encoded and encrypted DEK and the DEK’s tag and nonce stored in the user document (from step 1) and base 64 decode the three values.
  5. Decrypt the base 64 decoded and encrypted DEK (from step 4) with the KEK (from step3) and the base 64 decoded tag and nonce (from step 4).
  6. Retrieve the encrypted and base 64 encoded data and the data’s tag and nonce stored in the document (from step 1) and base 64 decode the three values.
  7. Decrypt the base 64 decoded and encrypted data (from step 6) with the decrypted DEK (from step 5) and base 64 decoded tag and nonce (retrieved from step 6)
  8. Store the KEK in the user session (from step 3)

Also, if you have stored the KEK in a key managements system as discussed in the part 1 of this blog post, then steps 3 and 4 would be replaced with fetching the KEK from the key management system.

A Continuation of the Proof of Concept

The Proof of Concept (PoC) from the previous blog post was about storing user documents that contain a username, hashed password, and an encrypted Social Security Number (SSN) in Elasticsearch in an index called users. In this blog post, I’m going to extend the PoC to fetch and decrypt the data.

The Design for the Proof of Concept Remains the Same

A logically cohesive class called Crypto will contain class methods that perform various cryptographic algorithms, such as encrypting data, decrypting data, and so on (Note: If you don’t know what cohesion is, than you can read my blog post on The 7 Types of Cohesion You Need To Know To Be The Best Software Engineer). 

A model class called User with the properties id, username, password, and SSN will be created when fetching and storing users into the Elasticsearch database. 

A Data Access Object (DAO) is used to interface with the Elasticsearch database when storing a user model object as a document in Elasticsearch and retrieving the user document and marshaling it into a user model object.

A class called ElasticsearchDb, which is a singleton class, that is used to initialize the Elasticsearch object once, which is used to get a database connection to Elasticsearch.

If you want to see the implementations for the Crypto, User, and ElasticsearchDb classes, then you can read my previous blog post How to Pythonically Store Sensitive and Encrypted Information in a Database for a Web Application the Right Way – Part 1. In this blog post, we need to update the Crypto and UserDao classes.

The Updated Crypto Class

The updated code for the Crypto class is the following:


from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes

import scrypt
import bcrypt

class Crypto:
    AES_KEY_SIZE = 32
    SALT_LENGTH = 32

    @classmethod
    def random_bytes(cls, key_size):
        return get_random_bytes(key_size)

    @classmethod
    def encrypt(cls, data, key):
        cipher = AES.new(key, AES.MODE_EAX)
        ciphertext, tag = cipher.encrypt_and_digest(data)
        nonce = cipher.nonce

        return (ciphertext,tag,nonce)

    @classmethod
    def decrypt(cls, ciphertext, nonce, tag, key):
        cipher = AES.new(key, AES.MODE_EAX, nonce)
        data = cipher.decrypt_and_verify(ciphertext, tag)
        return data

    @classmethod
    def key_derivation_function(cls, password, salt):
        # arg0 - password
        # arg1 - salt
        # arg2 - iteration count
        # arg3 - block size
        # arg4 - number of threads
        # arg5 - hash size

        return scrypt.hash(password, salt, 2**12, 2**3, 1, 32)
    
    @classmethod
    def hash_password(cls, password):
        password_bytes = bytes(password, 'ascii')
        salt = bcrypt.gensalt()

        hash = bcrypt.hashpw(password_bytes, salt)

        return hash
    
    # Verify the password.
    @classmethod
    def verify_password(cls, password, hash):
        return hash == bcrypt.hashpw(password, hash)

The only new class method that has been added to this class is the verify_password method.

The verify_password Method

The implementation for the verify_password method is the following:

    # Verify the password.
    @classmethod
    def verify_password(cls, password, hash):
        return hash == bcrypt.hashpw(password, hash)

This method will hash the plaintext password and compare it to the hashed password. If they are equal, then true is returned; otherwise, false is returned.

The Updated UserDao Class

The updated code for the UserDao class is the following:

from model import User
from crypto import Crypto

import base64
from elasticsearchdb import ElasticsearchDb

class UserDao:
    USER_INDEX = "user"
    DOC_SOURCE = "_source"
    HITS = "hits"
    ID = "_id"

    def __init__(self, es):
        self.__es = es

    # Insert New User into Database
    def insert_user(self, user:User):
        # 1. Generate the Data Encryption Key - used for encrypting user data.
        data_encryption_key = Crypto.random_bytes(Crypto.AES_KEY_SIZE)

        # 2. Encrypt the sensitive user data.
        encrypted_user_ssn, ssn_tag, ssn_nonce = Crypto.encrypt(bytes(user.ssn, "ascii"), data_encryption_key)

        # 3. Base 64 Encode the sensitive and encryted user data.
        b64_encrypted_user_ssn = base64.b64encode(encrypted_user_ssn).decode("ascii")
        b64_ssn_tag = base64.b64encode(ssn_tag).decode("ascii")
        b64_ssn_nonce = base64.b64encode(ssn_nonce).decode("ascii")

        # 4. Generate the Key Encryption Key - used for encrypting the data encryption key.
        kek_salt = Crypto.random_bytes(Crypto.SALT_LENGTH)
        key_encryption_key = Crypto.key_derivation_function(user.password, kek_salt)

        # 5. Encrypt the Data Encrytion Key
        encrypted_dek, dek_tag, dek_nonce = Crypto.encrypt(data_encryption_key, key_encryption_key)

        # 6. Base 64 Encode The Data Encryption Key
        b64_encrypted_dek = base64.b64encode(encrypted_dek).decode("ascii")
        b64_dek_tag = base64.b64encode(dek_tag).decode("ascii")
        b64_dek_nonce = base64.b64encode(dek_nonce).decode("ascii")

        password_hash = Crypto.hash_password(user.password).decode("ascii")

        b64_kek_salt = base64.b64encode(kek_salt).decode("ascii")

        resp = self.__es.index(index="user", id=user.id, document={
            "username" : user.username,
            "password" : password_hash,
            "ssn" : b64_encrypted_user_ssn,
            "ssn_tag": b64_ssn_tag,
            "ssn_nonce" : b64_ssn_nonce,
            "dek" : b64_encrypted_dek,
            "dek_tag" : b64_dek_tag,
            "dek_nonce" : b64_dek_nonce,
            "kek_salt" : b64_kek_salt
        })

        return resp


    # Authenticate the user when they login.
    def authenticate_user(self, username, password):
        user_doc = self.__fetch_user_doc(username)[self.DOC_SOURCE]
        
        if user_doc is None:
            return False
        
        hashed_password = bytes(user_doc["password"], "ascii")

        return Crypto.verify_password(bytes(password, "ascii"), hashed_password)


    # Fetch the user document from Elasticsearch.
    def __fetch_user_doc(self, username):
        response = self.__es.search(index=self.USER_INDEX, query = {
            "match" : {
                "username.keyword" : username
            }
        })

        if self.HITS not in response:
            return None
        
        hits = response[self.HITS]

        if self.HITS not in hits:
            return None

        hits = hits[self.HITS]

        if len(hits) < 1:
            return None

        user_doc = hits[0]

        return user_doc

    # Fetch the user object when they login.
    def get_user_on_login(self, username, password):
        # 1. Fetch the user document.
        user_hit = self.__fetch_user_doc(username)
        user_doc = None

        if user_hit is not None:
            user_doc = user_hit[self.DOC_SOURCE]
        else:
            return (None, None)
  
        username = user_doc["username"]
        hashed_password = user_doc["password"]

        # 2. Extract the kek salt and base 64 decode it.
        b64_kek_salt = user_doc["kek_salt"]
        kek_salt = base64.b64decode(b64_kek_salt)

        # 3. Regenerate the key encryption key.
        key_encryption_key = Crypto.key_derivation_function(password, kek_salt)

        # Steps 5-7 are inside __decrypt_ssn private method.
        dek = self.__decrypt_dek(user_doc, key_encryption_key)
        decrypted_ssn = self.__decrypt_ssn(user_doc, dek)
        
        user = User(user_hit[self.ID], username, password, decrypted_ssn.decode("ascii"))

        # 8. Return the user object and KEK. Note: The KEK should be stored in the user session.
        return (user, key_encryption_key)
    
    # Fetch the user with the KEK.
    def get_user_with_kek(self, username, kek):
        user_hit = self.__fetch_user_doc(username)
        user_doc = None

        if user_hit is not None:
            user_doc = user_hit[self.DOC_SOURCE]
        else:
            return None

        username = user_doc["username"]
        password = user_doc["password"]

        dek = self.__decrypt_dek(user_doc, kek)
        decrypted_ssn = self.__decrypt_ssn(user_doc, dek)


        user = User(user_hit[self.ID], username, password, decrypted_ssn.decode("ascii"))

        return user

    def __decrypt_dek(self, user_doc, kek):
        # 4. Fetch the base 64 encoded and encrypted DEK and tag and nonce, and base 64 decode them.
        b64_encrypted_dek = user_doc["dek"]
        b64_dek_tag = user_doc["dek_tag"]
        b64_dek_nonce = user_doc["dek_nonce"]

        encrypted_dek = base64.b64decode(b64_encrypted_dek)
        dek_tag = base64.b64decode(b64_dek_tag)
        dek_nonce = base64.b64decode(b64_dek_nonce)

        # 5. Decrypt the DEK
        dek = Crypto.decrypt(encrypted_dek, dek_nonce, dek_tag, kek)
        return dek

    # Decrypts the SSN
    def __decrypt_ssn(self, user_doc, dek):
        # 6. Fetch the base 64 encode and encrypted data and tag and nonce, and base 64 decode them.
        b64_ssn_tag = user_doc["ssn_tag"]
        b64_ssn_nonce = user_doc["ssn_nonce"]
        b64_encrypted_ssn = user_doc["ssn"]

        encrypted_ssn = base64.b64decode(b64_encrypted_ssn)
        ssn_tag = base64.b64decode(b64_ssn_tag)
        ssn_nonce = base64.b64decode(b64_ssn_nonce)

        # 7. decrypt the data and construct a user object.
        decrypted_ssn = Crypto.decrypt(encrypted_ssn, ssn_nonce, ssn_tag, dek)

        return decrypted_ssn

    # Determine if the user exists in the database.
    def user_exists(self, user):
        response = self.__es.search(index=self.USER_INDEX, query = {
            "match" : {
                "username.keyword" : user.username
            }
        })

        hit_count = len(response[self.HITS][self.HITS])

        return hit_count >= 1

Let’s go over the new methods added to the UserDao class.

The __fetch_user_doc method

First, let’s go over the private method __fetch_user_doc, which has the following implementation:

    # Fetch the user document from Elasticsearch.
    def __fetch_user_doc(self, username):
        # Query for the user document.
        response = self.__es.search(index=self.USER_INDEX, query = {
            "match" : {
                "username.keyword" : username
            }
        })

        if self.HITS not in response:
            return None
        
        hits = response[self.HITS]

        if self.HITS not in hits:
            return None

        hits = hits[self.HITS]

        if len(hits) < 1:
            return False

        user_doc = hits[0]

        return user_doc

The above method will fetch the user document given the username as an argument to the function. First, we query for the user document. Once we retrieve the JSON response from Elasticsearch, we need to extract the user document from the response. The following is an example of what they response of the query may look like:

{
  "took": 2,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 1,
      "relation": "eq"
    },
    "max_score": 1.2039728,
    "hits": [
      {
        "_index": "user",
        "_id": "1",
        "_score": 1.2039728,
        "_source": {
          "username": "gary.drocella",
          "password": "$2b$12$eS8ObWbYlcjtPBIB51JvluNnU5VSLC42HuEBpPu6.Yvj/8JVxyyjm",
          "ssn": "LLJCF1lrwSYpw+s=",
          "ssn_tag": "+oXWPSSjkQHx2P7FvRTitw==",
          "ssn_nonce": "WWFI+RGUEiXTNGqMgSkAAw==",
          "dek": "49ffEKw8Dut23QvGsyhI5QpMlyGboC81FR5FRKvjnlg=",
          "dek_tag": "XV3JA+E88Akfk+cL4jp+mg==",
          "dek_nonce": "e0rfaYUk5FDsV1ok0C5wrA==",
          "kek_salt": "ZlC9fQ1u4xowftR8FUINQzuJc1pO1hbtGb9WG6qWR+E="
        }
      }
    ]
  }
}

Therefore, to get the hit of the document, you need to access response[“_hits”][“_hits”][0]. The correct user will always be the first document returned because before a user is inserted, we determine if the username exists, and if it does, then we don’t insert the user into the database. The hit of the user document is returned. Next, let’s examine the authenticate_user method.

The authenticate_user Method

The authenticate_user method will authenticate the user when they login by taking in the username and password as arguments and comparing the stored hashed password with the hashed password of the login. The implementation of the authenticate_user method is the following:

    # Authenticate the user when they login.
    def authenticate_user(self, username, password):
        user_doc = self.__fetch_user_doc(username)[self.DOC_SOURCE]
        hashed_password = bytes(user_doc["password"], "ascii")

        return Crypto.verify_password(bytes(password, "ascii"), hashed_password)

First, the __fetch_user_doc method is used to get the user document. Then, the hashed_password stored in the user document is converted to bytes, and the password that is passed as an argument is converted to bytes. Both the hashed password and plaintext password are passed to the verify_password class method of the Crypto class. If the password is correct, then true will be returned, which means the user is authenticated; otherwise, false will be returned, which means the user is not authenticated.

The get_user_on_login Method

The implementation of the get_user_on_login method is the following:

    # Fetch the user object when they login.
    def get_user_on_login(self, username, password):
        # 1. Fetch the user document.
        user_hit = self.__fetch_user_doc(username)
        user_doc = None

        if user_hit is not None:
            user_doc = user_hit[self.DOC_SOURCE]
        else:
            return (None, None)
  
        username = user_doc["username"]
        hashed_password = user_doc["password"]

        # 2. Extract the kek salt and base 64 decode it.
        b64_kek_salt = user_doc["kek_salt"]
        kek_salt = base64.b64decode(b64_kek_salt)

        # 3. Regenerate the key encryption key.
        key_encryption_key = Crypto.key_derivation_function(password, kek_salt)

        # Steps 5-7 are inside __decrypt_ssn private method.
        dek = self.__decrypt_dek(user_doc, key_encryption_key)
        decrypted_ssn = self.__decrypt_ssn(user_doc, dek)
        
        user = User(user_hit[self.ID], username, password, decrypted_ssn.decode("ascii"))

        # 8. Return the user object and KEK. Note: The KEK should be stored in the user session.
        return (user, key_encryption_key)

If you read the above code with the comments, then it’s self-explanatory. First, the code with fetch the user document. Second, the code extracts the KEK salt and base 64 decodes it. Third, The KEK is regenerated. Fourth, the encrypted and base 64 encoded DEK is extracted and base 64 decoded, and the same is done with the tag and nonce. Fifth, the DEK is decrypted with the KEK, tag, and nonce. Sixth, the base 64 encoded and encrypted data and associated tag and nonce are retrieved and base 64 decoded. Seventh, the sensitive data is decrypted with the decrypted DEK. Eighth, the user object and KEK are returned as a tuple.

Note: Do not store any decrypted, sensitive data in the user’s session.

The get_user_with_kek Method

This method is used to fetch the user when the user is already logged in. The implementation of the get_user_with_kek method

    # Fetch the user with the KEK.
    def get_user_with_kek(self, username, kek):
        user_hit = self.__fetch_user_doc(username)
        user_doc = None

        if user_hit is not None:
            user_doc = user_hit[self.DOC_SOURCE]
        else:
            return None

        username = user_doc["username"]
        password = user_doc["password"]

        dek = self.__decrypt_dek(user_doc, kek)
        decrypted_ssn = self.__decrypt_ssn(user_doc, dek)

        user = User(user_hit[self.ID], username, password, decrypted_ssn.decode("ascii"))

        return user

This method performs the same actions as the get_user_on_login, but it doesn’t need to regenerate the KEK because the KEK is passed in as an argument from the user’s session.

The user_exists Method

The user_exists method will determine if a user with the username already exists in the database. The implementation of the user_exists method is the following:

    # Determine if the user exists in the database.
    def user_exists(self, user):
        response = self.__es.search(index=self.USER_INDEX, query = {
            "match" : {
                "username.keyword" : user.username
            }
        })

        hit_count = len(response[self.HITS][self.HITS])

        return hit_count >= 1

A query is performed on Elasticsearch to determine if any user with a given username is found. If the length of the array in the response of the query is greater than or equal to one, then a user with that username exists in the database. If a user with the username exists, then true is returned; otherwise, false is returned.

Running the Updated Proof of Concept

The following code is the updated PoC:

from model import User
from dao import UserDao
from elasticsearchdb import ElasticsearchDb

from dotenv import load_dotenv
import os

import time

def get_user_dao():    
    load_dotenv()
    es_password = os.environ.get("ELASTIC_PASSWORD")

    esdb = ElasticsearchDb("localhost", 9200, 
        protocol="https", 
        ca_certs="~/elasticsearch-8.11.2/config/certs/http_ca.crt",
        username="elastic",
        password=es_password
    )

    es = ElasticsearchDb.elasticsearch.fget(esdb)
    userDao = UserDao(es)
    return userDao

# Registers a new user - This code was discussed in Part 1 of the blog post.
def register_new_user(uid, username, password, ssn):
    userDao = get_user_dao()
    u = User(uid, username, password, ssn)
    resp = None

    if not userDao.user_exists(u):
        resp = userDao.insert_user(u)
    
    return resp

# Logs a user in.
def login(username, password):
    userDao = get_user_dao()

    # Authenticate the user.
    if not userDao.authenticate_user(username, password):
        return (None, None)

    # Getting a user object when the user logs in. Note: Do not store the user object in a user session because the user object has the
    # decrypted social security number, but do store the username and Key Encryption Key in the user session so that you can use the
    # get_user_with_kek function if you need to decrypt data while the user is logged in.
    user, kek = userDao.get_user_on_login(username, password)
    return (user, kek)

# Gets a user given a username and KEK from a user session. Note: This function is used after the user is already logged in.
def get_user_during_session(username, kek):
    userDao = get_user_dao()
    user = userDao.get_user_with_kek(username, kek)
    return user

def display_ssn(ssn):
    print("Displaying User SSN: " + ssn)

def main():

    es_response = register_new_user(1, "gary.drocella", "passw0rd", "000-00-0000")
  
    # Give Elasticsearch time to index the user document before "logging in"
    time.sleep(2)

    user,kek = login("gary.drocella", "passw0rd")
    
    if user is not None:
        display_ssn(user.ssn)

    if kek is None:
        return

    # Getting the user object when the user is already logged in by using the key encryption key and username stored in a session.
    same_user = get_user_during_session("gary.drocella", kek)

    if same_user is not None:
        display_ssn(same_user.ssn)


main()

Let’s examine the new functions added to the PoC.

The login Function

The login function should be invoked when the user logs into the web application. The implementation of the login function is the following:

# Logs a user in.
def login(username, password):
    userDao = get_user_dao()

    # Authenticate the user.
    if not userDao.authenticate_user(username, password):
        return (None, None)

    # Getting a user object when the user logs in. Note: Do not store the user object in a user session because the user object has the
    # decrypted social security number, but do store the username and Key Encryption Key in the user session so that you can use the
    # get_user_with_kek function if you need to decrypt data while the user is logged in.
    user, kek = userDao.get_user_on_login(username, password)
    return (user, kek)

After a User Dao object is created, the authenticate_user method is invoked to authenticate the user. If the user is authenticated, the get_user_on_login method is invoked, which will create a user object with decrypted data and key encryption key. Note: once the decrypted data is fetched, do what you need to do with it, but don’t store it in a user session. The KEK should be used if you need to get the decrypted data of the user again.

The get_user_during_session Function

This function will get the user after the user is already logged in. It uses the users username and KEK, which are passed as arguments, to do so. The following is the implementation of the get_user_during_session function:

# Gets a user given a username and KEK from a user session. Note: This function is used after the user is already logged in.
def get_user_during_session(username, kek):
    userDao = get_user_dao()
    user = userDao.get_user_with_kek(username, kek)
    return user

After we create a User Dao object, a user object is fetched by invoking the get_user_with_kek method of the User Dao, and the user is returned.

Key Take Aways

  • The reason we use a data encryption key is so that when the user changes their password, we don’t need to decrypt and re-encrypt all the data, which could be a very expensive operation.
  • If the user does change their password, then you will need to decrypt the data encryption key with the KEK generated from the old password, and then regenerate a new KEK with the new password they changed it to, and encrypt the DEK with the new KEK.
  • Do not store decrypted data in the user session. Use the KEK from the user session and the get_user_with_kek method of the User Dao to get the decrypted data again.
  • If you’re going to store the KEK somewhere, then use a Key Management System, and ensure that the KEK is kept separate from the encrypted DEK and encrypted data.

That concludes this two-part blog post on storing sensitive and encrypted information in a database the right way. If you found this blog post helpful, then share the post, subscribe to my blog, and buy me a coffee.

Subscribe

* indicates required

Intuit Mailchimp

References

AES encryption & decryption in Python: Implementation, modes & key management. (n.d.). Onboardbase.com. Retrieved January 4, 2024, from https://onboardbase.com/blog/aes-encryption-decryption/

AES-GCM authenticated encryption. (2023, January 1). Cryptosys.net. https://www.cryptosys.net/pki/manpki/pki_aesgcmauthencryption.html

Bernstein, C., & Cobb, M. (2021, September 24). Advanced Encryption Standard (AES). Security; TechTarget. https://www.techtarget.com/searchsecurity/definition/Advanced-Encryption-Standard

Choice of authenticated encryption mode for whole messages. (n.d.). Cryptography Stack Exchange. Retrieved January 4, 2024, from https://crypto.stackexchange.com/questions/18860/choice-of-authenticated-encryption-mode-for-whole-messages

Data Encryption Key. (2014). Techopedia.com. https://www.techopedia.com/definition/5660/data-encryption-key-dek

Dennis, Y. (2023, February 21). PyCryptodome: Secure your data with ease. Geek Culture. https://medium.com/geekculture/pycryptodome-secure-your-data-with-ease-4d70817fae7

Grigutytė, M. (2023, June 16). What is bcrypt and how does it work? NordVPN. https://nordvpn.com/blog/what-is-bcrypt/

Hinch, D. (2023, October 3). Understanding Nonces and Their Use in AES-GCM. Linkedin.com. https://www.linkedin.com/pulse/understanding-nonces-use-aes-gcm-derek-hinch/

How to encypt sensitive data in database of a web app? (n.d.). Information Security Stack Exchange. Retrieved January 4, 2024, from https://security.stackexchange.com/questions/166286/how-to-encypt-sensitive-data-in-database-of-a-web-app/166288

Key-encryption-key (KEK) – glossary. (n.d.). Nist.gov. Retrieved January 4, 2024, from https://csrc.nist.gov/glossary/term/key_encryption_key

Singleton pattern in python – A complete guide. (2020, October 30). GeeksforGeeks. https://www.geeksforgeeks.org/singleton-pattern-in-python-a-complete-guide/

What Are Key Derivation Functions? (2023, June 22). Baeldung.com. https://www.baeldung.com/cs/kdf-cryptography

What is a cryptographic “salt”? (n.d.). Cryptography Stack Exchange. Retrieved January 4, 2024, from https://crypto.stackexchange.com/questions/1776/what-is-a-cryptographic-salt

What is a Message Authentication Code (MAC)? (n.d.). Fortinet. Retrieved January 4, 2024, from https://www.fortinet.com/resources/cyberglossary/message-authentication-code

What is the difference between Pycrypto’s Random.get_random_bytes and a simple random byte generator? (n.d.). Stack Overflow. Retrieved January 4, 2024, from https://stackoverflow.com/questions/22395478/what-is-the-difference-between-pycryptos-random-get-random-bytes-and-a-simple-r