I’d like some feedback on an encrypting algorithm. I’m not rolling my own, but even putting together well-proved pieces may create many pitfalls. (I don’t know if I'm stretching questioning in this forum. I'm after concrete feedback on concrete, potential pitfalls.)
- Cold storage of encrypted cryptocurrency seeds (i.e. secret keys).
EDIT: The seed (as used in this question) is the super secret plaintext. But often called seeds, as it is normally a binary array, and not ascii.
The purpose is the same as for BIP0038 (* see summary below).
Likelihood of someone reusing a passphrase is definitely present.
The algorithm should be as simple as possible, to avoid errors in implementations.
- Generate cryptographically random seed (32 bytes) (Or input from user*)
* EDIT: The seed is 32 random bytes. User may input a cryptographically random seed, generated somewhere else, if she wants to. The seed is used to generate public addresses. A bit like private/public key pairs. It's the private key, the seed, that I would like to encrypt and store.)
- Input UTF-8 passphrase from user, applying canonical normalization
- Generate cryptographically random salt (12 bytes)
- (Possibly ask for an input on “toughness”, to adjust the argon2id parameters.)
- Generate 32 byte encryption key, using Argon2id, using 2) and 3).
I guess the argon2id options is a separate topic altogether.
I think iterations = 2(something + toughness)
Encrypt the seed 1) with AES-CTR, using salt from 3) as counter, and encryption key from 5).
Encrypted seed (6) + Salt (3) is concatenated and base32 encoded to uppercase, to make the most compact QR code, appended by ‘:T’ + <toughness>.
My reasoning and Questions
Any tripwires I’ve tangled myself into?
AES-GCM is preferred over AES-CTR. GCM has message authentication. But I don’t see mallability as an attack vector. You might as well burn the piece of paper, instead of scribbling on the QR code.
A) Are GCM and CTR equally good in this use-case? Both have a catastrophic failure if the same key and counter is re-used. (No difference there, that I am aware.)
B) Birthday problem. Why 12 byte salt?
Open for input. :)
Reading Birthday attack - Wikipedia, and if my math is correct:
An 8 byte salt would yield a likely birthday collision every 5x10^9 reuse of the passphrase.
A 12 byte salt would yield a likely birthday collision every 3x10^14 reuse of the passphrase.
But it will fit in the same size QR code, after base32 encoded. (Version 3, ECC level L.)
C) A 16 byte salt is still better. But is it needed?
Any considerations regarding Argon2id?
D) Is there a problem that the salt is reused for two different operations? (Both Argon2id and AES)
E) AES CTR vs CBC? The seed is 32 bytes long, so no padding is needed for CBC. Both need a random counter/iv. Are they equally good for this use-case, or does one of them have a better fit for the problem?
Why base32? Because QR has an optimized encoding for uppercase and digits. And security here is more than hacking. An unreadable QR code is just as disastrous.
Keepass is all very good for this too.
My use-case is scannable paper. And that my wife can use it if I get hit by a bus or fall off a cliff. :)
I apologize if this post being a bit long. But it's putting the steps together that causes the security to break.
Any comment are welcoming. :)
* The BIP38. https://en.bitcoin.it/wiki/BIP_0038 Uses scrypt, and 2xsha-ing the passphrase first. The salt derives from the passphrase, so reusing the passphrase is an issue.
BIP38 feels overly complicated. Like someone tying lots of bad knots, instead of tying one knot properly to secure the load. Simplicity is also security.