0

For about 20 years, I've been using gnupg to encrypt a plain-text file containing my passwords. (The actual per-site passwords are generated using a hash function.) It's worked OK for me, but gnupg is a very heavy-weight system that's really designed for public-key encryption, with all the complexity that entails. I've experienced various hassles with it over the years, and because of its complexity I find it hard to reason about its behavior or about what would be good practices to follow when I use it.

For this reason, I decided I wanted to try switching to something extremely simple as a way of encrypting my password file. The solution I came up with was just to write a short ruby script that does symmetric encryption based on a password. Since I wanted to avoid all the complexity and hassle of maintaining a gnupg keyring, I decided that I would just include a simple feature for guarding against encryption using a mistyped password -- it generates a "check word" from your password, and if the check word isn't what you're used to seeing, you know you made a typo.

A lot of smart people have put a lot of effort into designing systems to serve this type of purpose, so I did this with some trepidation. Are there known pitfalls to this approach, which I am not appreciating? In my implementation, I made up a list of pros and cons as I saw them, but my expertise in crypto and security is pretty low, so it may be that I'm not understanding correctly and I'm doing something really dumb.

This is all for use in a terminal, on a desktop linux machine that I control and that doesn't run software that I don't control. I would not try to use anything like this on a phone.

1
  • 1
    Instead of using a 'check word', you might want to consider using an AES encryption mode, that has built-in authentication checking, such as AES-GCM. Also, you may want to consider using openssl for symmetric encryption - it's simple to do using one command, without maintaining keyrings, etc. See stackoverflow.com/questions/16056135/… for more info.
    – mti2935
    Commented Mar 26, 2023 at 16:19

1 Answer 1

1

One major worry with the "check word" idea is that it's vulnerable to shoulder-surfing. That shouldn't be a problem since the check word isn't inherently valuable, except unfortunately it's vastly easier to recover the actual password - if you have something (like the check word) to check against - than it should be.

The main problem in your password-handling code is that you're using fast hashes. MD5 is deprecated and shouldn't be used for literally anything anymore (its specific weaknesses are probably not relevant to how you're using it but... why risk it?) but even a very modern secure hash like SHA3-512 would be inappropriate by itself in either place you use hashes (generating the cipher key and generating the check word index). The problem is that these hash functions are designed to be extremely fast and use as little RAM as possible, which is important when you're using them in things like embedded systems or for creating and checking MACs on every TLS frame, but mean they're extremely vulnerable to parallelization. A modern high-end GPU can compute over ten billion of such hashes, each on a different candidate password, per second (the practical limit is lower because the limitation is getting the candidates in and then the digests back and checking if one of them produced the desired result, but it's still fast enough to make brute-forcing the password far easier than you probably expect).

The check word, in effect, saves the attacker a lot of time (if they know what its expected value is). They can simply run candidate passwords through MD5, see which ones produce a hash that leads to the correct index, and only then perform the more expensive check of trying to generate the decryption key, use it, and seeing if the file decrypts cleanly (it'll be pretty obvious if it does; plain text has way fewer bits of entropy per byte than the pseudo-random result of a cipher with the wrong key, and it's thus pretty trivial to programmatically check the output for "does this seem like a plausible password file?"... and that's without even getting into checking the padding, which is usually effectively a very quick checksum). Skipping this expensive check for the vast majority of candidates means a GPU can go on cranking through candidates at close to full speed.


The solution is to use a password hashing / key derivation function, which is a special class of hash functions designed to be slow, computationally expensive, and (in newer cases) memory intensive and resistant to side channel attacks. The current leading one is Argon2id, a derivative of the winner of a multi-year competition to advance the state of the art, but even the venerable (and memory-easy thus parallelizable) PBKDF2 (Password-Based Key Derivation Function 2) would be a vast improvement from your current approach. All password hashing functions include at least one tunable cost parameter (sometimes called the "work factor") which determines how computationally expensive hashing a single password is. For a system like yours, there's no reason to not use the most expensive cost you can personally stand to wait for, whether that's a quarter second or 15 seconds (the same work factor will of course take less time on faster hardware or in faster languages/implementations, though). Modern KDFs also include a tunable memory cost, which should again be as large as you can afford to make it; at the very least it ought to be large enough to force any CPU or GPU to use something beyond L1 or even L2 cache (this will drastically impair parallelization) so go for at least a few megabytes, but for your case in particular it's probably fine to make it take a gigabyte if you want (unless you're running this on very low-end hardware that won't have a gig of spare RAM). With high enough compute and memory costs, even a mediocre password can resist brute-force attacks for quite a long time.

Obviously, you need to use the KDF for both the check word index and the key itself (using a fast hash for the key isn't safe, no matter how slow the check word generation is, because the attacker will just switch to trying decryption directly and checking padding/text-ishness of the file). Assuming you want to keep the check word, since you want the check word digest and the key to be different, you should use a different salt for each one. The salts - like the IV - are not secret (you can and should include them, in plain text, in the encrypted file) but should be random.

Speaking of things that should be in the file in plain text, you should have some form of actual data integrity check, rather than just password correctness check, on your encrypted files. There are a number of ways to do this, but the usual ones are computing and including a Message Authentication Code (typically computed using a hash function, that is, an HMAC), or Authenticated Encryption (often abbreviated AEAD, Authenticated Encryption with Associated Data) cipher mode. HMAC-SHA2-256 or similar are fine, and there's some slight discussions to be had about whether you should use the same key for the HMAC as for the cipher and what exactly should be HMACed (the plaintext or the ciphertext or something else) but having one at all will be a big improvement. For AEAD, the most common modes are GCM (Galois/Counter Mode) for block ciphers (like AES), or Poly1305 (usable with any cipher but most common with the X20 "Latin Dances" stream cipher family, as in XChaCha20-Poly1305). In either case, you'll get an additional bit of data - the authentication code - which should be stored in plain text along with the ciphertext (typically appended or prepended to the ciphertext in the same file). Such authentication codes allow you to not only detect an incorrect key, they can also detect if the ciphertext was modified, even by a careful attacker (and with much higher confidence than just checking padding).


So... with all that said, is there some reason you don't want to just use a dedicated password manager app, even a purely local one like KeePass? Cryptography is really hard to get right, I'm sure there's stuff I missed even in this long response. Leave it to people who have done this a lot, and whose work has been extensively reviewed by other experienced people.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .