Introduction
There's a saying in information security that we shouldn't roll our own. The general idea behind this is that somewhere during the development of your new protocol or implementation of an existing protocol, you will make some major security mistake which severely impacts the security of the system.
However, this project is for educational purposes to learn about existing protocols, implementation issues and general programming. I am aware that I have almost certainly made some severe mistake(s) while developing YAPM, so I recommend against using this as your personal password manager (identification of any security issues would be very much appreciated in the comments!).
Password managers are very useful as they not only remove the problem of forgetting passwords, but also encourage stronger, unique passwords for each account as they do not need to be remembered. However, this also means that the master password has to be very secure - if this is found out, then an attacker has the keys to the kingdom so to speak.
Security
General Security
YAPM passwords are stored in a file called main_store using AES-GCM-256 bit encryption, and a cryptographically random 32 byte encryption key is used for encrypting and decrypting the stored passwords. This encryption key is encrypted with AES-GCM-256 bit, using the key as a hash of the master password, and stored in a file called enc_key. Users are authenticated with an Argon2i hash (and salt) of the master password which is stored in a file called auth. Argon2 is very slow, resulting in a delay of a few seconds at login - but that is exactly what we want: The algorithm to hash the password needs to be slow so that brute-force attacks take a long time to find the password used to create the hash. A mistake here would be to use a fast hash algorithm which isn't designed for password storage, such as MD5 or SHA-256.
All files are stored in %appdata%/YAPM.
Authentication & Some More Details
YAPM uses Libsodium (C# wrapper) as the cryptographic library.
To access passwords stored with YAPM, the user's master password (which has to be at least 15 characters) is required. This master password is validated against the verification hash, which is an Argon2i hash of the user's master password with the following parameters: time=6, memory=131072KiB, parallelism=1 (these parameters come from the predefined value in Libsodium PasswordHash.StrengthArgon.Moderate
). I chose Argon2 as it is the winner of the recent password hashing competition and is currently widely recommended.
If the master password has been successfully validated against the verification hash, the master password is used to decrypt the encryption key. This 32 byte encryption key is generated by the cryptographically random function, SodiumCore.GetRandomBytes()
in Libsodium. This key is then encrypted with AES-GCM-256 bit encryption with the key being a 32 byte hash of the master password (GenericHash.Hash()
).
This decrypted 32 byte encryption key is then used to decrypt the passwords, which are encrypted with AES-GCM-256 bit. Unique 12 byte nonces are used for all AES operations and for encryption of the main store, the nonce increments to ensure it is never used twice.
Using the Code
The program is a winforms application which can either be executed within the Visual Studio environment or directly from the executable file. The code is quite heavily commented, but if you have any specific question, please feel free to ask and I will provide more details.
There are 9 main classes used:
MainForm
- Contains the main user form and handles interactions with the form. AddPasswordForm
- Contains the form to add a password, handles the creation of a new password. EditPasswordEntryForm
- Contains the form to edit a password entry, handles the modification of an existing password entry. SettingsForm
- Contains the setting form which is used to change, view and read/write the settings to the stored file (%appdata%/YAPM/settings). The content of the settings file follows a strict format of setting=value. Crypto
- Handles all cryptographic operations with the aid of Libsodium. Purposes of this class include authenticating a user, encryption/decryption of stored passwords, changing the user's master password. Register
- Creates initial files required before storing passwords (encryption key, nonce, hash, default settings). PasswordManage
- Manages loaded passwords from file (displaying passwords, editing passwords, adding/deleting passwords). Watchdog
- Creates a watchdog timer which causes the program to timeout and lock the application if the user is idle for a certain time period. When the mouse moves over a form, the watchdog timer is reset. ByteOperation
- Increments / decrements a byte. This is used to increment /decrement the 12 byte nonce used for encryption.
The process of encryption and decryption of the main store is shown below:
private static byte[] GetKey(string masterPassword) {
byte[] encryptedKey = Convert.FromBase64String(File.ReadAllText(YAPM_KEY));
byte[] keyNonce = Convert.FromBase64String(File.ReadAllText(YAPM_KEY_NONCE));
byte[] key = SecretAeadAes.Decrypt(encryptedKey, keyNonce,
GenericHash.Hash(masterPassword, (byte[])null, 32));
return key;
}
private static string[] GetStoreFileContents() {
string[] storeFileContents = File.ReadAllLines(YAPM_STORE);
return storeFileContents;
}
private static byte[] GetNonce() {
byte[] nonce = Convert.FromBase64String(File.ReadAllText(YAPM_STORE_NONCE));
return nonce;
}
public static void EncryptStoreFile(string masterPassword, string[] dataToEncrypt) {
byte[] nonce = GetNonce();
byte[] key = GetKey(masterPassword);
File.WriteAllText(YAPM_STORE, "");
for (int i = 0; i < dataToEncrypt.Length; i++) {
ByteOperation.Increment(ref nonce);
byte[] byteDataToEnc = Encoding.ASCII.GetBytes(dataToEncrypt[i]);
var encrypted = SecretAeadAes.Encrypt(byteDataToEnc, nonce, key);
File.AppendAllText(YAPM_STORE, Convert.ToBase64String(encrypted) + Environment.NewLine);
}
File.WriteAllText(YAPM_STORE_NONCE, Convert.ToBase64String(nonce));
}
public static List<string> DecryptStoreFile(string masterPassword) {
string[] storeFileContents = GetStoreFileContents();
byte[] nonce = GetNonce();
byte[] key = GetKey(masterPassword);
List<string> decryptedList = new List<string>();
for (int i = storeFileContents.Length - 1; i > -1; i--) {
byte[] dataToDecrypt = Convert.FromBase64String(storeFileContents[i]);
var decrypted = SecretAeadAes.Decrypt(dataToDecrypt, nonce, key);
decryptedList.Add(Encoding.ASCII.GetString(decrypted));
ByteOperation.Decrement(ref nonce);
}
return decryptedList;
}
As you can see in EncryptStoreFile
and DecryptStoreFile
, each item to encrypt/decrypt in the array is encrypted using the same key, but a different nonce.
While all encryption / decryption is handled with Libsodium, nonces are dealt with ByteOperation.cs. This class has two simple methods:
ByteOperation.Increment()
public static void Increment(ref byte[] byteArr) {
for (int i = 0; i < byteArr.Length; i++) {
byteArr[i]++;
if (byteArr[i] > 0) {
return;
}
}
return;
}
ByteOperation.Decrement()
public static void Decrement(ref byte[] byteArr) {
for (int i = 0; i < byteArr.Length; i++) {
byteArr[i]--;
if (byteArr[i] < 255) {
return;
}
}
return;
}
Screenshots
I've gone with a pretty basic design on the program as I didn't want the user interface to look too cluttered and untidy.
Main form once logged in (this is fully re-sizeable). Rows can be right clicked to show more options:
Settings menu:
Adding a new password:
Editing an existing password entry:
Points of Interest
While writing this article, I was looking over the code I showed in the 'Using the code' section and noticed something wasn't quite right, specifically, the following:
ByteOperation.Increment(ref nonce);
File.WriteAllText(YAPM_STORE, "");
for (int i = 0; i < dataToEncrypt.Length; i++) {
byte[] byteDataToEnc = Encoding.ASCII.GetBytes(dataToEncrypt[i]);
var encrypted = SecretAeadAes.Encrypt(byteDataToEnc, nonce, key);
File.AppendAllText(YAPM_STORE, Convert.ToBase64String(encrypted) + Environment.NewLine);
}
File.WriteAllText(YAPM_STORE_NONCE, Convert.ToBase64String(nonce));
Notice how the nonce is incremented, and then that same nonce is used to encrypt each password entry in the array. This is bad as you now have multiple ciphertexts encrypted using the same keystream, which is insecure. I fixed this problem by incrementing the nonce during each iteration of the for
loop while encrypting, and decrementing the nonce during each iteration of the for
loop while decrypting:
public static void EncryptStoreFile(string masterPassword, string[] dataToEncrypt) {
byte[] nonce = GetNonce();
byte[] key = GetKey(masterPassword);
File.WriteAllText(YAPM_STORE, "");
for (int i = 0; i < dataToEncrypt.Length; i++) {
ByteOperation.Increment(ref nonce);
byte[] byteDataToEnc = Encoding.ASCII.GetBytes(dataToEncrypt[i]);
var encrypted = SecretAeadAes.Encrypt(byteDataToEnc, nonce, key);
File.AppendAllText(YAPM_STORE, Convert.ToBase64String(encrypted) + Environment.NewLine);
}
File.WriteAllText(YAPM_STORE_NONCE, Convert.ToBase64String(nonce));
}
public static List<string> DecryptStoreFile(string masterPassword) {
string[] storeFileContents = GetStoreFileContents();
byte[] nonce = GetNonce();
byte[] key = GetKey(masterPassword);
List<string> decryptedList = new List<string>();
for (int i = storeFileContents.Length - 1; i > -1; i--) {
byte[] dataToDecrypt = Convert.FromBase64String(storeFileContents[i]);
var decrypted = SecretAeadAes.Decrypt(dataToDecrypt, nonce, key);
decryptedList.Add(Encoding.ASCII.GetString(decrypted));
ByteOperation.Decrement(ref nonce);
}
return decryptedList;
}
Features
YAPM has a timeout which is very important - without this if somebody leaves their computer, they have pretty much handed over ownership of all their account to any passer-by with malicious intentions. The timeout works by starting a watchdog timer (this is contained in Watchdog.cs) which resets ("kicks the watchdog") whenever the mouse is moved over the form. When the watchdog timer reaches the timeout time, the application locks by signing out. The timeout time can be changed in the settings and has a default of 2 minutes (120000 ms). The timeout can also be disabled in the settings if unwanted.
Confirming the current master password when changing passwords is also important - without this somebody can transfer ownership of the account to themselves, which would also lock the user out of their account.
After logging in with your master password, passwords will be obfuscated by default (this can be changed in the settings) and can be displayed by right clicking on the row the password is on -> Password visibility -> Show. Right clicking is also used to edit / delete passwords, copy contents from the password entry and show any notes.
History
- 08/03/2018 - Initial post