Click here to Skip to main content
15,115,261 members
Articles / Security
Article
Posted 8 Mar 2018

Stats

14.7K views
1.3K downloads
15 bookmarked

YAPM (Yet Another Password Manager)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
10 Mar 2018CPOL6 min read
This article describes the security techniques required to create a secure offline password manager and how the Libsodium library has been used to achieve this. YAPM stores passwords with AES encryption and authenticates users with an Argon2 hash.

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.

Image 1

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:

C#
 // Function to get the encryption key for the main store.
private static byte[] GetKey(string masterPassword) {
    // Get encrypted encryption key and nonce.
    byte[] encryptedKey = Convert.FromBase64String(File.ReadAllText(YAPM_KEY));
    byte[] keyNonce = Convert.FromBase64String(File.ReadAllText(YAPM_KEY_NONCE));
    // Decrypt key with parameters stored in YAPM_PATH.
    byte[] key = SecretAeadAes.Decrypt(encryptedKey, keyNonce, 
                 GenericHash.Hash(masterPassword, (byte[])null, 32));
    return key;
}

// Function to get the contents of the main store file.
private static string[] GetStoreFileContents() {
    // Read each line from file. String array contains base64 encoded strings.
    string[] storeFileContents = File.ReadAllLines(YAPM_STORE);
    return storeFileContents;
}

// Function to get the nonce for the main store.
private static byte[] GetNonce() {
    byte[] nonce = Convert.FromBase64String(File.ReadAllText(YAPM_STORE_NONCE));
    return nonce;
}

// Function to encrypt the main store.
public static void EncryptStoreFile(string masterPassword, string[] dataToEncrypt) {
    // Get required variables.
    byte[] nonce = GetNonce();
    byte[] key = GetKey(masterPassword);
    // Clear main store.
    File.WriteAllText(YAPM_STORE, "");
    // Encrypt each password entry.
    for (int i = 0; i < dataToEncrypt.Length; i++) {
        // Increment nonce so every password entry uses a different nonce.
        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);
    }
    // Write nonce to file.
    File.WriteAllText(YAPM_STORE_NONCE, Convert.ToBase64String(nonce));
}

// Function to decrypt the main store.
public static List<string> DecryptStoreFile(string masterPassword) {
    // Get required variables.
    string[] storeFileContents = GetStoreFileContents();
    byte[] nonce = GetNonce();
    byte[] key = GetKey(masterPassword);
    List<string> decryptedList = new List<string>();
    // Decrypt each password entry. Work backwards with last nonce used, as nonce decrements.
    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));
        // Decrement nonce to get each nonce used to encrypt password entry.
        ByteOperation.Decrement(ref nonce);
    }
    // Return list containing all decrypted password entries.
    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()

C#
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()

C#
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:

Image 2

Settings menu:

Image 3

Adding a new password:

Image 4

Editing an existing password entry:

Image 5

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:

C#
// Increment nonce.
ByteOperation.Increment(ref nonce);
// Clear main store.
File.WriteAllText(YAPM_STORE, "");
// Encrypt each password entry.
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);
}
// Write nonce to file.
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:

C#
// Function to encrypt the main store.
public static void EncryptStoreFile(string masterPassword, string[] dataToEncrypt) {
    // Get required variables.
    byte[] nonce = GetNonce();
    byte[] key = GetKey(masterPassword);
    // Clear main store.
    File.WriteAllText(YAPM_STORE, "");
    // Encrypt each password entry.
    for (int i = 0; i < dataToEncrypt.Length; i++) {
        // Increment nonce so every password entry uses a different nonce.
        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);
    }
    // Write nonce to file.
    File.WriteAllText(YAPM_STORE_NONCE, Convert.ToBase64String(nonce));
}

// Function to decrypt the main store.
public static List<string> DecryptStoreFile(string masterPassword) {
    // Get required variables.
    string[] storeFileContents = GetStoreFileContents();
    byte[] nonce = GetNonce();
    byte[] key = GetKey(masterPassword);
    List<string> decryptedList = new List<string>();
    // Decrypt each password entry. Work backwards with last nonce used, as nonce decrements.
    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));
        // Decrement nonce to get each nonce used to encrypt password entry.
        ByteOperation.Decrement(ref nonce);
    }
    // Return list containing all decrypted password entries.
    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Joe Dillon
Student
United Kingdom United Kingdom
I've done some projects on InfoSec and other general programming, some are uploaded here on CodeProject.

One of which is taps, a CLI Linux system security audit tool.

Comments and Discussions

 
BugDictionary Attacks Pin
Member 1026666311-Jul-19 12:58
MemberMember 1026666311-Jul-19 12:58 
GeneralRe: Dictionary Attacks Pin
Joe Dillon11-Jul-19 23:27
MemberJoe Dillon11-Jul-19 23:27 
Hi Konrad,

Thanks for looking at my project. It's fine to post your findings right here!

Joe
GeneralRe: Dictionary Attacks Pin
Member 1026666315-Jul-19 7:11
MemberMember 1026666315-Jul-19 7:11 
GeneralRe: Dictionary Attacks Pin
Joe Dillon15-Jul-19 8:37
MemberJoe Dillon15-Jul-19 8:37 
GeneralRe: Dictionary Attacks Pin
Member 1026666316-Jul-19 6:59
MemberMember 1026666316-Jul-19 6:59 
QuestionHi Joe, thank you! Pin
Member 968816721-Nov-18 7:29
MemberMember 968816721-Nov-18 7:29 
AnswerRe: Hi Joe, thank you! Pin
Joe Dillon26-Nov-18 8:21
MemberJoe Dillon26-Nov-18 8:21 
GeneralMy vote of 5 Pin
Nick_31415926549-Mar-18 6:20
MemberNick_31415926549-Mar-18 6:20 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.