Search code examples
javascriptnode.jscryptographynode-crypto

Does Node's crypto decipher always throw an error if the password is wrong?


I'm creating a little game-like feature for fun (so no need to be super secure).

On the server, I encrypt using a secret password:

const key = crypto.createCipher('aes-256-cbc', 'secret')
let encryption = key.update('cheerio', 'utf8', 'hex')
encryption += key.final('hex')

On the client, I want the user to guess the password (which is secret in this example), like this:

try {
    const key = crypto.createDecipher('aes-256-cbc', guess)
    let decryption = key.update(encryption, 'hex', 'utf8')
    decryption += key.final('utf8')
 }
catch(err) {
    console.log("err", err)
 }

The question is, will this always throw an error if the guess fails, or do I need to compare it to my phrase ('cheerio')? (I am yet to guess something that doesn't throw an error.)

I feel the former would give an extra layer of security, because then there's both a secret password AND secret word, while if I do a compare on the client the secret word is exposed in the code.


Solution

  • No, an error is not always generated when guess fails.

    The reason for the error (error:06065064:digital envelope routines:EVP_DecryptFinal_ex:bad decrypt), is a wrong padding during decryption, here. The crypto module uses PKCS7 padding by default. In this padding variant each padding byte has the value that corresponds to the number of padding bytes, e.g. 01, 0202, 030303, 04040404 etc., see here for details. Decryption with an incorrect password will result in random/corrupted plaintext. Its end will generally not correspond to a valid PKCS7 padding, which causes the error. However, by chance a plaintext may result, whose end matches a valid PKCS7 padding. In this case, no error is raised. E.g. a 01 byte terminating the plaintext would be compatible with PKCS7 padding, which occurs for a random password with a probability of 1/256. The same applies to 0202 with a probability of 1/65536, and so on.

    This can be easily verified if padding is disabled during decryption (key.setAutoPadding(false)). Then no error is raised. The same applies when using a stream cipher mode (e.g., CTR) where padding is implicitly disabled.

    This means that even in cases where no error is produced, a comparison with the plain text is necessary.

    Note that createCipher (deprecated, by the way) derives a key and an IV from the password. The same password (and thus the same key and IV) will generate the same ciphertext for the same plaintext. Therefore, a comparison of the ciphertexts serves the same purpose as a comparison of the plaintexts.

    One more thing, the results of update and final must be concatenated, otherwise the result is generally not complete, i.e. in encryption = key.final('hex') the = must be replaced by +=, although this is probably just a typo. The same applies to decryption.