samuel-lucas6 / Cahir

A deterministic password manager.
GNU General Public License v3.0
4 stars 0 forks source link
challenge-response deterministic-password-generator deterministic-password-manager deterministic-password-safe dpg dpm keyfile password-generator password-manager pepper yubikey yubikey-otp

Cahir

A deterministic password manager.

cahir

Installation

On Windows, Linux, and macOS (x64 and ARM64), you can use the pre-built binaries:

https://github.com/samuel-lucas6/Cahir/releases

If your system has the latest .NET 8 SDK, you can build from source (replace RID with your platform):

$ cd src
$ dotnet publish -r RID -c Release

If you try to use your YubiKey on Linux, you'll likely get an error mentioning libudev.so. To get things working, you must create a symbolic link from libudev.so.1 to the directory .NET is using for Cahir. On Debian-based distros, the command will look something like this:

$ sudo ln -s /usr/lib/x86_64-linux-gnu/libudev.so.1 /home/samuel/.net/cahir/ftlSfb7AS_XKCtW7BksiMKLSs1L2dYc=/libudev.so

Usage

USAGE:
    cahir [OPTIONS]

EXAMPLES:
    cahir -i "alicejones@pm.me" -d "github.com"
    cahir -i "alicejones@pm.me" -d "github.com" -p "correct horse battery staple"
    cahir -i "+44 07488 855302" -d "github.com" -f "password.txt"
    cahir -i "+44 07488 855302" -d "github.com" -k "pepper.key"

OPTIONS:
    -i, --identity <IDENTITY>     Your unique identifier (e.g. email address)
    -d, --domain <DOMAIN>         The website domain (e.g. github.com)
    -p, --password <PASSWORD>     Your master password (omit for interactive entry)
    -f, --password-file <FILE>    Your master password stored as a file (omit for interactive entry)
    -k, --keyfile <FILE>          Your pepper stored as a file
    -g, --generate <FILE>         Randomly generate a keyfile with the specified file name
    -y, --yubikey [SLOT]          Use your YubiKey for challenge-response (defaults to slot 2)
    -m, --modify-slot [SLOT]      Set up a challenge-response YubiKey slot
    -c, --counter <COUNTER>       The counter for when a site password needs to be changed (default is 1)
    -l, --length <LENGTH>         The length of the derived site password (default is 20 characters or 8 words)
    -a, --lowercase               Include lowercase characters in the derived site password
    -u, --uppercase               Include uppercase characters in the derived site password
    -n, --numbers                 Include numbers in the derived site password
    -s, --symbols                 Include symbols in the derived site password
    -w, --words                   Derive a passphrase
    -v, --version                 Prints version information
    -h, --help                    Prints help information

Specification

Security Goals

Threat Model

Cahir aims for security against an adversary who does not have physical or remote access to the user's machine. With such access, security cannot be guaranteed because the adversary has compromised the device. For example, they can use hardware/software keyloggers, memory forensics, disk forensics, and so on. However, Cahir attempts to zero sensitive data to minimise the risk of retrieval from memory or disk, and there is some protection against shoulder surfing.

To guess user inputs, the adversary must either perform:

  1. An online attack against a specific site, which should be hindered by rate limiting and Cahir's password hashing/pepper derivation.
  2. An offline attack against a specific program, which should be hindered by the program's and Cahir's password hashing/pepper derivation.
  3. An offline attack against a specific derived site password, which requires a derived site password to be leaked and should be hindered by Cahir's password hashing/pepper derivation.

For security, we assume that:

Cryptographic Algorithms

Master Key Derivation

salt = BLAKE2b-256(context || identity)
masterKey = Argon2id(password, salt, memorySize, passes, parallelism)

Pepper Derivation

Keyfile

pepper = BLAKE2b-256(context || keyfile)

YubiKey

challenge = BLAKE2b-256(key: masterKey, message: context || counter || length || characterSet || domain)
response = HMAC-SHA1(key: yubikeySecret, message: challenge || padding)
pepper = BLAKE2b-256(key: response, message: context)

Site Key Derivation

siteKey = BLAKE2b-256(key, message: context || counter || length || characterSet || domain)

Site Password Derivation

ciphertext = ChaCha20(plaintext, nonce, key, counter)

if lowercase
    characterSet += "abcdefghijklmnopqrstuvwxyz"

if uppercase
    characterSet += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

if numbers
    characterSet += "0123456789"

if symbols
    characterSet += "!#$%&'()*+,-./:;<=>?@[]^_`{}~"

for i = 0 to length
    randomIndex = LE128(ciphertext.Slice(start: i * 16, length: 16)) % characterSet.Length
    sitePassword[i] = characterSet[randomIndex]

Site Passphrase Derivation

ciphertext = ChaCha20(plaintext, nonce, key, counter)

offset = 0
characterSet = "0123456789"
randomNumber = 0
randomPosition = 0
if numbers
    offset = 2
    randomNumber = LE128(ciphertext.Slice(start: 0, length: 16)) % characterSet.Length
    randomPosition = LE128(ciphertext.Slice(start: 16, length: 16)) % wordCount

count = 0
wordlist = BIP39.Split("\n")
for i = 0 to wordCount
    randomIndex = LE128(ciphertext.Slice(start: (i + offset) * 16, length: 16)) % wordlist.Length
    word = wordlist[randomIndex]

    for j = 0 to word.Length
        if uppercase and j == 0
            sitePassphrase[count] = word[j].ToUpper()
        else
            sitePassphrase[count] = word[j]
        count++

    if numbers and i == randomPosition
        sitePassphrase[count] = characterSet[randomNumber]
        count++

    if i != wordCount - 1
        if symbols
            sitePassphrase[count] = "-"
        else
            sitePassphrase[count] = " "
        count++