What this usually means
Most Node.js crypto errors come from three categories: (1) using an algorithm that your OpenSSL version has deprecated or disabled (e.g., SHA1 in FIPS mode, or insecure ciphers like RC4), (2) passing mismatched key types or lengths – for example, giving a 128-bit key to AES-256, or a PEM string where a Buffer is expected, (3) encoding mismatches in the input data, like passing a hex-encoded string without specifying the encoding. Node.js relies on the underlying OpenSSL library, so changes in OpenSSL 3.0 (Node 18+) broke many legacy crypto patterns.
The first ten minutes — establish facts before touching code.
- 1Run 'node -e "console.log(process.versions.openssl)"' to check OpenSSL version. If >=3.0, many legacy algorithms are disabled by default.
- 2Check the exact error message. If it contains 'EVP_UNSUPPORTED', it's an algorithm that OpenSSL doesn't allow. Use 'openssl list -cipher-algorithms' to see what's available.
- 3Verify key lengths: AES-128 requires 16 bytes, AES-256 requires 32 bytes. 'Buffer.byteLength(key)' to check.
- 4For 'bad decrypt', check if the IV and key match the encryption parameters. A common mistake is using a different IV during decryption.
- 5If using crypto.createSign or createVerify, make sure the key is a valid PEM string (including line breaks) and the algorithm matches (e.g., 'RSA-SHA256' not 'sha256WithRSAEncryption').
- 6For PBKDF2/Scrypt errors, check the salt length (should be >=16 bytes) and iteration count (should be >100k for PBKDF2).
The specific files, logs, configs, and dashboards that usually own this bug.
- searchThe exact stack trace in the console (first line of the error)
- searchYour code that calls crypto.createCipheriv, crypto.createDecipheriv, crypto.createSign, etc.
- searchThe key and IV generation logic – are they using the right encodings (hex, base64, utf8)?
- searchOpenSSL configuration: /etc/ssl/openssl.cnf or the NODE_OPTIONS environment variable
- searchNode.js version: 'node -v' – some crypto APIs changed in Node 12, 15, 18
- searchPackage.json: if using a crypto wrapper library like 'node-forge' or 'crypto-js', check their compatibility
Practical causes, not theory. These are the things you will actually find.
- warningOpenSSL 3.0 legacy provider disabled (Node 18+): algorithms like MD4, MD5, RIPEMD160, or RC4 are not available by default
- warningWrong key length for the chosen algorithm (e.g., 24-byte key for AES-256, or 16-byte key for AES-192)
- warningPassing PEM key as a single-line string without newlines (common when copying from env variables)
- warningUsing crypto.createHash with an unsupported algorithm like 'md5' (case-sensitive, should be 'MD5' but often disabled)
- warningMismatched input encoding: encrypting with 'hex' output but decrypting with no encoding specified
- warningUsing a deprecated cipher like 'aes-128-ecb' which is considered insecure and may be removed
Concrete fix directions. Pick the one that matches your root cause.
- buildSwitch to a supported algorithm: replace 'aes-256-cbc' with 'aes-256-gcm', or 'sha1' with 'sha256'
- buildExplicitly enable legacy algorithms by setting NODE_OPTIONS='--openssl-legacy-provider' (temporary fix, use only if necessary)
- buildValidate key length before use: if (key.length !== 32) throw new Error('Key must be 32 bytes for AES-256')
- buildNormalize PEM keys by ensuring proper line breaks: key.replace(/\n/g, '\n').concat('\n')
- buildAlways specify encoding explicitly: cipher.update(data, 'utf8', 'hex') and decipher.update(data, 'hex', 'utf8')
- buildUse async crypto.pbkdf2 or crypto.scrypt instead of Sync variants to avoid blocking the event loop
A fix you cannot prove is a guess. Close the loop.
- verifiedRun the encryption/decryption in a test with known inputs and verify output matches expected hash or ciphertext
- verifiedCheck that no errors are thrown and the result is consistent across multiple runs
- verifiedUse 'openssl enc -aes-256-cbc -d -in encrypted.bin -out decrypted.txt -K <keyhex> -iv <ivhex>' to verify against the Node.js output
- verifiedFor signing, verify with 'openssl dgst -sha256 -verify public.pem -signature sig.bin data.txt'
- verifiedRun the code in a container with the same Node.js version to ensure environment consistency
Things that make this bug worse or harder to find.
- warningDon't set --openssl-legacy-provider in production without understanding the security implications (it re-enables weak algorithms)
- warningDon't hardcode keys or IVs in source code – use environment variables or a secrets manager
- warningDon't ignore the 'key object' API introduced in Node 11.6 – using strings for keys is deprecated in newer versions
- warningDon't use ECB mode for encryption – it's not semantically secure
- warningDon't assume all OpenSSL algorithms are available – check Node.js documentation for supported list
- warningDon't ignore the difference between createCipher (deprecated) and createCipheriv (recommended)
Production decryption failure after Node.js upgrade from 16 to 18
Timeline
- 09:15Deploy Node.js 18 upgrade to staging
- 09:20Monitoring alerts show 500 errors on /decrypt endpoint
- 09:22Error logs show 'ERR_OSSL_EVP_UNSUPPORTED' for crypto.createDecipheriv
- 09:30Check OpenSSL version: 3.0.7 – legacy provider disabled
- 09:35Review encryption code: using 'aes-256-cbc', which is still supported
- 09:45Discover that cipher name string is 'aes-256-cbc' but algorithm constant is 'AES-256-CBC' – case-sensitive
- 09:50Fix algorithm string to uppercase, redeploy
- 09:52Decryption works, error rate drops to zero
We were upgrading our Node.js runtime from version 16 to 18 to get the latest features and security patches. After deploying to staging, our /decrypt endpoint immediately started returning 500 errors. The error logs showed 'ERR_OSSL_EVP_UNSUPPORTED' when calling crypto.createDecipheriv. I initially panicked, thinking OpenSSL 3.0 had removed AES-256-CBC, but a quick check showed it's still supported.
I spent 15 minutes checking the algorithm list and even considered using --openssl-legacy-provider, but that felt like a bandaid. Then I noticed the error message included the algorithm name as passed to createDecipheriv: 'aes-256-cbc' (lowercase). In Node.js 16, OpenSSL 1.1.1 was case-insensitive, but OpenSSL 3.0 is strict. The algorithm string must be exactly 'AES-256-CBC'.
I changed the algorithm to uppercase, redeployed, and the decryption worked immediately. The lesson: never assume string casing is irrelevant, especially across OpenSSL versions. We also added a validation step that checks the algorithm against a whitelist of supported ciphers before use.
Root cause
Algorithm string 'aes-256-cbc' (lowercase) worked in OpenSSL 1.1.1 but is rejected by OpenSSL 3.0 which requires exact casing.
The fix
Changed algorithm string to uppercase 'AES-256-CBC' and added validation against supported ciphers.
The lesson
OpenSSL 3.0 is stricter about algorithm names. Always use the canonical names from the OpenSSL documentation, and test crypto code after Node.js upgrades.
OpenSSL 3.0 introduced a provider-based architecture. By default, only the 'default' provider is loaded, which includes most common algorithms (AES, SHA256, etc.). Legacy algorithms like MD5, RC4, and Blowfish are in the 'legacy' provider, which is not loaded by default.
Node.js 18 uses OpenSSL 3.0. If your code calls crypto.createHash('md5'), you'll get 'ERR_OSSL_EVP_UNSUPPORTED'. To enable legacy algorithms, you can set the environment variable NODE_OPTIONS='--openssl-legacy-provider', but this is a security risk. Better to migrate to modern algorithms.
Many crypto errors come from passing the wrong key length. AES-128 needs 16 bytes, AES-192 needs 24 bytes, AES-256 needs 32 bytes. If you pass a 24-byte key to AES-256, you'll get 'bad key length'.
Additionally, when you use crypto.createCipheriv, the IV must be exactly 16 bytes for CBC mode. Using a shorter IV will throw an error. Always generate keys and IVs using crypto.randomBytes and store them as hex or base64.
When encrypting, you specify input encoding (e.g., 'utf8') and output encoding (e.g., 'hex'). When decrypting, you must reverse them: input encoding is 'hex', output encoding is 'utf8'. A common mistake is to pass the ciphertext as a string without specifying encoding, leading to 'bad decrypt'.
For signing and verification, the data and signature must be in the expected format. crypto.createSign expects data as a Buffer or string with encoding. If you pass a hex-encoded hash, it will be treated as raw bytes unless you specify 'hex'.
Since Node.js 11.6, the crypto module supports KeyObject instances. Passing a string as a key is deprecated and may cause issues. Instead, use crypto.createPublicKey(keyString) or crypto.createPrivateKey(keyString) to get a KeyObject, then pass it to sign/verify or encrypt/decrypt functions.
KeyObject also validates the key format early, giving you clearer error messages. For example, if you pass an invalid PEM, it throws 'ERR_OSSL_PEM_BAD_BASE64_DECODE' immediately.
Frequently asked questions
Why does crypto.createCipheriv throw 'ERR_OSSL_EVP_UNSUPPORTED' for a cipher that should work?
This often means the cipher name is incorrect or uses a different case. OpenSSL 3.0 is case-sensitive. Check the exact name with 'openssl list -cipher-algorithms' and ensure you use the canonical name (e.g., 'AES-256-CBC' not 'aes-256-cbc'). Also, some ciphers may be in the legacy provider.
How do I fix 'error:1E08010C:lib(15):EVPCipherInit:bad key length'?
This error means the key length doesn't match the algorithm. For AES-256, the key must be exactly 32 bytes. Use Buffer.byteLength(key) to check. If your key is a string, its byte length depends on encoding. Prefer using Buffer.from(key, 'hex') or 'base64' to ensure correct length.
Is it safe to use --openssl-legacy-provider in production?
No. It re-enables weak algorithms like MD5 and RC4, which are disabled for security reasons. Only use it as a temporary measure during migration. Instead, update your code to use modern algorithms.
How can I check which OpenSSL algorithms are available in my Node.js environment?
Run 'openssl list -cipher-algorithms' and 'openssl list -digest-commands' in your terminal. For Node.js, you can programmatically check: crypto.getCiphers() and crypto.getHashes(). Note that getCiphers() returns the names Node.js recognizes, which may differ slightly.
What is the difference between createCipher and createCipheriv?
createCipher (deprecated) derives the IV from the key internally, which is insecure. createCipheriv requires you to provide an explicit IV, which should be random and unique per encryption. Always use createCipheriv (and createDecipheriv) for proper security.