Verifying decrypted string

Viewed 273

I am using the Node.js crypto library to encrypt larger files using AES-256-ctr. One problem that I am having right now is verifying if the decryption was successful or not. Currently, if I use the wrong password I have no way of knowing the decryption failed until I look at the text and realize it's unreadable, but this makes it difficult to programmatically check and ask for a password retry.

What I have gathered so far from my research (please correct me if I am wrong):

  • I can use HMAC to verify the integrity of the decrypted message
  • I should always run the HMAC function after encryption, as hashing anything before encrypting the data and not encrypting the hash itself defeats the purpose of encryption
  • Don't use the same secret key for encryption and MAC

My questions: Would it be wrong, and if so why, to just prepend/append a string to the message I want to encrypt then check if it's there when I am decrypting. This seems like a simple check to make, but I can't wrap my head around why it shouldn't be done.

Ex.

Encryption:

  1. 'hello world'
  2. (prepend said string) 'ENCRYPTED:hello world'
  3. (encrypt message)
  4. 'iv:abc123'

Decryption:

  1. 'iv:abc123' -> (decrypt)
  2. decStr = 'ENCRYPTED:hello world' or 'ab/12c879312sc/!/'
  3. if (decStr.split(':')[0] === 'ENCRYPTED') { correct decryption }

What should I use for the 'secret' in the createHmac() function? I don't want to require the user to hold two passwords. One for encryption and one for verification. Can I use the derived key i get from encrypting the password using scrypt?

I can't tell if this is a stupid question or if I'm just not searching the right thing, but some of the similar questions to this just say to use aes-gcm to avoid having to worry about this. I'm mostly just curious about how to do it.

1 Answers

You probably don't want to just prepend a string to the message. That's because an attacker can tamper with the message outside of the initial string and you still can't detect it. Because CTR mode uses XOR with a keystream for encryption, this is trivial.

You want to do one of two things: either use an AEAD to encrypt, such as AES-GCM or ChaCha20-Poly1305, or use CTR mode with an HMAC over the encrypted data. Note that if you do this, you must never re-use an encrypted key-nonce pair, or you lose all security.

The simplest, easiest way to do this is to use AES-GCM. You can generate a single, cryptographically secure key, in an appropriate way. Then, generate a random 256-bit salt from the system CSPRNG. Then do this (|| is concatentation):

PRK = HMAC-SHA-256(salt, initial-key)
key = HMAC-SHA-256(PRK, "encryption key" || 0x01)
IV = HMAC-SHA-256(PRK, "nonce" || 0x01)

This uses HKDF to generate an encryption key, an IV (which you'll need to truncate to 12 bytes). Encrypt the data, and emit the 32-byte salt and the encrypted message.

This way, as long as you always pick a random salt for each encryption, whether you use the same initial key or not doesn't matter, since you'll practically never re-use the key-IV pair.

If these files are so large that you can't cheaply wait to check the entire encryption to see if they're correct, then add an additional 32-byte value:

check-value = HMAC-SHA-256(PRK, "check value" || 0x01)

Then, emit the salt, check value, and data encrypted with AES-GCM. To verify the password, check that the check value is the same, and if not, it's wrong. All of these approaches can also be done with ChaCha20-Poly1305 if that's more appropriate to your environment (ARM systems or other systems without AES acceleration).

The extra 0x01 byte is strictly not necessary, but using it means you're using HKDF, which means that you can say you're using a standard approach that's well respected.

Related