An object which contains a dictionary of passwords (key = identifier, value = password).
A class that uses a cryptographically secure random number generator to hash a user supplied password with a unique salt. Can also be used with a preexisting salt to determine if two hashes match.
Takes in byte data and encrypts it with a supplied key. Returns the cipher text, and a cryptographically random nonce and authentication tag.
Takes in the cipher text, salt, nonce and authentication tag, and packs everything into a single byte array. Also handles unpacking.
Not directly shown but lies in-between “Create random password” and “Internal vault”. Uses a cryptographically secure random number generator to generate random passwords.
This flow describes the process of creating a new vault.
This flow describes the process of saving a vault. AES GCM is used to ensure authenticity and integrity of the cipher text. Alternatively, CTR or CBC could have been used alongside a method such as Encrypt-then-MAC.
This flow describes the process of opening a vault.
The front end for this application is structured like a state machine. Depending on which state you are currently in, you can only move to other predetermined states (see the diagram below). The primary reason for doing this was to try make the console nature of this application easy to understand.
Welcome -> Create Vault: "Creating a Vault"
Welcome -> Open Vault: "Opening a Vault"
Vault Running -> Welcome: "Saving a Vault"
To guide my decision in choosing a random number generator for this application, I read the OWASP Cryptographic Storage Cheat Sheet. This document states that a Cryptographically secure pseudo-random number generator should be used when dealing with cryptography, and that in .NET / C#, I should use the RNGCryptoServiceProvider class instead of the standard Random class.
RNGCryptoServiceProvider is better suited for password generation because it provides a more secure random function that is not as repeatable as Random (at the expense of speed).
The graph below shows a distrubution of generated characters (50 million 80 character passwords). The least common character was k with 42,538,130 occurances, while the most common character was w with 42,572,116 occurrences (a difference of 33,986).
For this application, the master password is used as the encryption key (after going through a KDF (Key Derivation Function)). This KDF required the following features:
Protection against rainbow table attacks (by salting the password)
Variable password length = fixed output length (for the key)
Sufficient computation time to prevent brute force guessing every possible password
To meet these requirements, I choose PBKDF2 as my key derivation function. PBKDF2 is used by other large firms such as 1Password for their master password hashes and is recommended by OWASP. The C# class that provides this functionality is "Rfc2898DeriveBytes".
The password is salted with a 32 cryptographically secure random byte array to avoid rainbow-table attacks (precomputed hash tables which aid in discovering passwords) and is hashed using HMAC-SHA512 with 200,000 iterations. The use of SHA512 and many iterations combined makes the hashing function fairly slow. This increases the time it takes to hash every possible password combination.
The password vault is stored as a *.vault (or any user supplied extension) file. This binary file contains the encrypted passwords, alongside the password hash salt, nonce, and authentication tag. The C# class "DataPacker" is used to pack and unpack the binary file into usable byte arrays. See the table below for a visual representation of the file.
|Cipher Text||Salt||Nonce||Authentication Tag|
|Variable Bytes||32 Bytes||12 bytes||16 bytes|
The cipher text is generated using AES GCM (Galois/Counter Mode) as per the OWASP cryptographic storage recommendations. The GCM cipher mode provides both data authenticity (integrity) and confidentially. GCM does not require the use of an external "Encrypt-then-MAC" scheme which is commonly used during CBC and CTR cipher modes (as they don’t provide any guarantees on the authenticity of the encrypted data unlike GCM).
The key used for AES GCM is a 256bit value obtained from a Key Derivation Function (KDF). This KDF is mentioned in more detail in the previous section, "Master Password Authentication". In summary, the key is derived from the user’s master password going through a PBKDF2-HMAC-SHA512 function with 200,000 iterations.
In order to access the vault, the user must supply the same master password which they used to first create the vault. Without this master password, the contents of the vault cannot be decrypted (the salt, nonce and tag alone cannot decrypt the vault and are considered public information). The users master password, generated hash, or key are never stored to disk.
Once the plain text is obtained, a System.Text.Json serializer is used to turn the raw bytes into a c# class which contains all the passwords (and vice versa).
An attacker may gain physical access to the vault file which contains all the encrypted passwords. Even if the attacker knows the file layout (cipher text, salt, nounce and tag), they cannot gain access to the file without brute forcing the master password. A unique salt (randomly generated with a cryptographically secure method) ensures that Rainbow Table attacks (containing precomputed hashes) cannot be used against the vault. The KDF ensures the attacker must spend x amount of time to generate each password, further lessoning the appeal of brute force attack.
There are two main attack vectors that I considered out of scope for this application and did not design a way to prevent them. Key loggers and memory dumps.
There is no way for me to detect if a key logger is on the user’s system, or prevent the users input from being detected when they are entering their master password. If a key logger can gain access to the user’s master password, then the users password vault is compromised (and likely everything on their system regardless).
If a malicious user or program is scanning or dumping system memory, an attacker may be able to access to traces of the user’s master password (although not explicitly stored anywhere, it is sent through methods), the key used to encrypt the vault (stored in memory while the vault is open) and all the users encrypted passwords (stored as an object in memory while the vault is open). I’ve considered this out of scope as if someone is scanning / dumping system memory, nothing is safe and the whole system is compromised.
Additionally, due to the C# Garbage collector, after closing a vault, the vault contents and key may still hang out in memory. These variables are set to null within the application, but the garbage collector will decide when to get rid of them.
When running the program on macOS, you may get an error like “No usable version of libssl was found”.
Personally, I already had OpenSSL installed, I added the following environment variable:
export DYLD_LIBRARY_PATH=/usr/local/opt/[email protected]/lib
This application has been tested to run on Windows 10 v21H1, and macOS Big Sur, you will need the .NET 6 SDK installed. Simply running the following commands should work:
dotnet run –project PasswordManager
You may need to prepend the export above if using macOS (export … && dotnet …)