I am attempting to secure a private key for remote storage on an untrusted device. Here are the parameters and my assumptions, I'd like to confirm I'm correct with them.

The keypair that I wish to encrypt is generated with ECDSA using the P-256 curve. My understanding is that this curve has 128 bits of security.

I give the user a Diceware passphrase (using cryptographically secure entropy) consisting of 10 words. This has 129 bits of security (12.9 bits per word).

I use HKDF with SHA-256 (no salt, no "info" label) to turn this passphrase into an actual key. The WebCrypto API dictates that I also specify the algorithm I intend to use this key for. I specify AES-CTR with a key length of 128 bits. The derivation function then outputs 128 bits of data (presumably truncated from 256 bits from the SHA-256 HMAC used by HKDF). Should I use the full 256 bits here instead?

Finally, I wrap the key using AES-CTR with a new empty 16-length byte array counter and a length of 128. According to the WebCrypto docs that I could find, length is a value from 1 to 128. What does this length represent and am I using it correctly?

In conclusion, my assumptions: The passphrase has 129 bits of security, it gets fed to a key derivation function that preserves 128 bits of security, and then this 128-bit key is used with AES-CTR which I'm hoping someone can confirm preserves the 128 bits of security as well.

Therefore, can I conclude that the encrypted private key being public is as secure as the public key being public? Deriving the private key from the public is 128 bits of security hard and so is decrypting the private key.


1 Answer 1

  1. You should store a salt for HKDF-Hash alongside the encrypted message. That way, the adversary can't attack many passwords simultaneously.

  2. You should use your application's name, and the purpose of the key, as the info for HKDF-Extract. That way, even if you are tempted in the future to use the same password for another purpose, it will be clearer what you must do to avoid accidentally reusing the key.

  3. It may be OK to use a 128-bit key, but unless you're budget-constrained, you should use a 256-bit key to avoid multi-target attacks on AES. There's nothing wrong with using a larger key than your target security level.

  4. The way that AES-CTR works is that for a key $k$ and a nonce $n$, it generates a one-time pad $\operatorname{AES}_k(n \mathbin\Vert 0) \mathbin\Vert \operatorname{AES}_k(n \mathbin\Vert 1) \mathbin\Vert \cdots$, and xors that with your message to get the ciphertext.

    What does $n \mathbin\Vert 0$ mean, exactly? It's the concatenation of some bit encoding of a per-message nonce, which may be anywhere from 0 to 128 bits, with some bit encoding of a per-block counter, whose length must be $128 - \ell_n$ where $\ell_n$ is the maximum number of bits in the nonce. The point is that we never reuse $\operatorname{AES}_k(x)$ for different parts of the plaintext.

    In the WebCrypto API, §25.3 AesCtrParams dictionary, your obligation is to pass a byte array [0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b; 0x00 0x00 0x00 0x00] that will serve as the initial input with the nonce, and then specify how long the counter is to be incremented as an integer, marked in this example by a semicolon.

    First, you must never use a nonce more than once with any given key. If your key is used only once ever, then you can just use an all-zero or empty nonce. If you reuse a key, then with AES, you should use a sequentially incrementing nonce, or think very carefully about what limits your application can live with on the number of messages you use any particular key with. In this case, I recommend a single-use key; if you want to use the same password for different purposes, just derive an independent key using a different HKDF-Expand info parameter.

    Second, the number of bits in the counter must fit the length of the longest message you can encounter. That is, if you handle up to $\ell_m$-bit messages, then you must specify a counter length in bits that is at least $\lceil\log_2 \ell_m/128\rceil$. Here we divide by 128 because each counter value corresponds to 128 bits of AES-CTR key stream.

  5. Actually, you may not want to use AES-CTR directly because it does not provide authentication. Unless you are again budget-constrained, or for some reason you want to avoid authentication (which makes sense in some applications, like a scheme to require 2FA for disk encryption so that the adversary can't even test a password guess without a separate physical token, but seems unlikely here), you should always use authenticated encryption by default absent a particular reason not to: AES-GCM, not AES-CTR.

  6. Beware timing side channels in software implementations of AES (and GHASH, if you use AES-GCM)! Even though hardware AES support is widespread today, it takes a lot of effort to audit your software stack well enough to guarantee it is reliably using the hardware AES support on all platforms that may be of interest to you. If you use NaCl crypto_secretbox_xsalsa20poly1305 instead, this is not a problem.

  • $\begingroup$ Thank you for your reply! I am looking into using AES-GCM now, which should also clear up some of my questions re AES-CTR (although I appreciate your responses) since GCM seems to only require a random 12 byte IV rather than a counter and length param. However, I do have one concern now. Your point number 6. Timing attacks I thought were tied to servers leaking information to attackers. But I intend to give the encrypted private key to any user who wants it and let them attempt to decrypt it using the 128 bit passphrase. Am I misunderstanding timing attacks or is my thinking flawed here? $\endgroup$ Commented Jan 1, 2018 at 19:55
  • $\begingroup$ @SamuelHorwitz For GCM, the nonce is always 96 bits and the counter is always 32 bits. (Technically you can pass in nonces of other sizes—but they are hashed into a nonce of 96 bits, so there's no advantage to a larger nonce but a false sense of security.) Side channel attacks may apply in any context—for example, maybe a script that doesn't have access to the key can cause your script in another context to attempt to decrypt an attacker-controlled private key with the real key, repeatedly, and measure how long it takes, from which timing they may be able to deduce bits of the secret key. $\endgroup$ Commented Jan 1, 2018 at 20:02
  • $\begingroup$ @SamuelHorwitz Proving negatives about whether malicious scripts can or can't cause your script to attempt to decrypt attacker-controlled private keys may be considerably harder than just using crypto_secretbox_xsalsa20poly1305, for which it is not hard to get confidence that there are no timing side channels. $\endgroup$ Commented Jan 1, 2018 at 20:03
  • $\begingroup$ Yeah you are right, it's probably easier to use libsodium in Javascript. I was sticking with the WebCrypto native stuff originally, but as long as the secure randomness function is used, a non-native encryption scheme is fine. Thanks again! $\endgroup$ Commented Jan 1, 2018 at 20:10
  • $\begingroup$ @SamuelHorwitz …actually, I should revise that: I don't have much confidence in the state of defenses against timing attacks on any JavaScript rendition of libsodium, but then neither do I have much confidence in the state of defenses against timing attacks on anything in JavaScript. Cryptography in JavaScript is still a pretty risky business. Caveat emptor. $\endgroup$ Commented Jan 1, 2018 at 20:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.