keyper 0.6.1

TUI password manager
Documentation

Keyper

A basic password manager with a TUI interface.

Storage

The sled database is used for on-disk storage. However, sled does not natively support encryption.

Instead, we use an encryption scheme composed from well-tested primitives implemented in the RustCrypto cryptographic libraries.

Salt Encryption

A salt is a cryptographic value used to add randomness to inputs, and is commonly used in key-derivation algorithms.

We use the following scheme for password-based key derivation (in pseudo-code):

SALT = {random 32-bytes}
PASSWORD = {user-input password}
ENCRYPTION_KEY = sha3::TurboShake256::hash(SALT | PASSWORD);

However, we also want to keep the salt value secret when storing to disk (maybe a bit of paranoia is a good thing? :3)

The following scheme is used to encrypt the salt for storage (in pseudo-code):

SALT = {random 32-bytes}
PASSWORD = {user-input password}

SALT_KEY = sha3::TurboShake256::hash(argon2id::hash(PASSWORD | <32-bytes of zeroes>));
SALT_NONCE = {random 12-bytes}
SALT

ENCRYPTED_SALT = chacha20poly1305::encrypt(SALT_KEY, SALT_NONCE, SALT)

The entry is then stored in the database as:

SALT_DB_KEY = sha3::TurboShake256::hash("salt");
sled::Db::insert(SALT_DB_KEY, SALT_NONCE | ENCRYPTED_SALT);

Password Key-derivation

The user-input password is combined with the random salt to derive the encryption key for database entries:

SALT = (random 32-bytes from previous step)
PASSWORD = (user-input password)

DB_ENCRYPTION_KEY = sha3::TurboShake256::hash(argon2id::hash(PASSWORD | SALT))

The DB_ENCRYPTION_KEY is never stored in the database, even in an encrypted form.

It is only stored in-memory to decrypt database entries when loading them into memory, and encrypting new entries for storage in the database.

Entry Encryption

Entries are encrypted in much the same way as the database SALT, with the slight change that the fields need to be length-prefix encoded.

We currently use 32-bit, little-endian length fields.

TITLE = Entry.title;
CONTENT = Entry.content;
TITLE_LEN = (TITLE.len() as u32).to_le_bytes();
CONTENT_LEN = (CONTENT.len() as u32).to_le_bytes();
ENTRY_NONCE = {random 12-bytes}
ENTRY_INDEX = EntryList.len()
ENTRY_PLAINTEXT = TITLE_LEN | TITLE | CONTENT_LEN | CONTENT

ENTRY_DB_KEY = sha3::TurboShake256::hash(ENTRY_INDEX)
ENCRYPTED_ENTRY = chacha20poly1305::encrypt(DB_ENCRYPTION_KEY, ENTRY_NONCE, ENTRY_PLAINTEXT)

sled::Db::insert(ENTRY_DB_KEY, ENTRY_NONCE | ENCRYPTED_ENTRY) 

Using the length-prefixed value encoding is very common, and allows for extending the Entry fields almost indefinitely.

By using the AEAD ChaCha20Poly1305 algorithm, we also get the benefit of database corruption protection.

If someone messes with your database entries, they won't decrypt properly.

Credits

Fuck AI

This application was made with 100% human engineering, entirely without the aid of LLMs.