The same AES key gives two different answers, and here is why
You encrypt a string here, paste the result into a server script with what looks like the identical key, and get garbage back. Nothing is broken. The word "key" is doing two jobs, and most browser encryption tools never tell you which one they picked.
Type hunter2 into a key box and AES has a problem: it needs 16, 24, or 32 bytes of key material, and you gave it seven. Something has to bridge the gap. That something is a key derivation function, and its choice of salt, digest, and round count is part of the ciphertext's identity. Change the derivation and the same passphrase produces a completely different key.
The console above makes the choice visible instead of hiding it. Passphrase mode derives the key the way openssl enc has done for decades. Hex key mode skips derivation entirely and feeds your bytes straight into the cipher.
Passphrase mode against hex key mode
Passphrase
Your text goes through EVP_BytesToKey with a random 8 byte salt, producing both the key and the IV. The salt is written into the output behind the ASCII marker Salted__, which is why every result starts with U2FsdGVkX1 once base64 encoded.
Encrypt the same words twice and the two outputs differ, because the salt is new each run. Both decrypt correctly. This is the format Node's crypto examples, CryptoJS defaults, and old OpenSSL command lines all speak.
Hex key
Your hex string becomes the key with no processing, so a 64 character string is a 256 bit AES key exactly as written. The IV is yours to supply and gets stored alongside the ciphertext rather than inside it.
This is the mode to pick when the other side is openssl enc -K -iv, PHP's openssl_encrypt, Python's cryptography package, or a Java SecretKeySpec. Those all take raw bytes and none of them expect a salt header.
Decryption in the console checks for the header before doing any work. Ciphertext with Salted__ and a hex key selected fails immediately with an explanation rather than returning empty output, which is the failure people spend an afternoon on.
Cipher modes, and why ECB is the wrong default
A block cipher encrypts 16 bytes at a time. The mode decides what happens between blocks, and that decision leaks more than most people expect.
| Mode | IV | Padding | What to know |
|---|---|---|---|
| ECB | None | Required | Each block is encrypted on its own, so identical plaintext blocks give identical ciphertext blocks. Structure in the input survives into the output. Present here for reading legacy data, not for protecting anything. |
| CBC | Required | Required | Each block is mixed with the previous one before encryption. The default choice, and the mode almost every existing system uses. Reusing an IV with the same key across messages weakens it. |
| CFB | Required | Applied here | Turns the block cipher into a stream, so standard implementations produce output the same length as the input. Read the padding note below before matching this against another language. |
| OFB | Required | Applied here | Also a stream, with the keystream generated independently of the data. A repeated key and IV pair destroys the security completely, so never reuse the pair. |
| CTR | Required | Applied here | Encrypts a counter to make the keystream, which allows random access into the ciphertext. Same warning as OFB about reusing the counter start. |
Watch what happens with a real example. Set ECB, then encrypt ABCDEFGHIJKLMNOPABCDEFGHIJKLMNOP with any key. The 32 character input is two identical 16 byte blocks, and the ciphertext repeats itself in the middle. Switch to CBC and the repetition disappears. That visible pattern is the entire reason ECB stays out of new work.
The padding trap in CFB, OFB, and CTR
Textbooks describe those three modes as producing ciphertext exactly as long as the plaintext, because there is nothing to pad. The CryptoJS library behind this page pads them anyway, applying whatever the padding box says to every block cipher mode without exception.
The result interoperates until you look closely. Encrypt a 17 byte string with AES-256-CFB and PKCS#7 here, and the output is 32 bytes. PHP decrypts it with the right key and hands back 17 bytes of your text followed by fifteen 0x0f bytes, because openssl_decrypt in a stream mode has no padding to strip:
// PKCS#7 selected here, decrypted in PHP
496e7465726f7020636865636b203132330f0f0f0f0f0f0f0f0f0f0f0f0f0f0fSet padding to None before encrypting in CFB, OFB, or CTR and the output drops to 17 bytes, matching openssl enc byte for byte. The console warns you about this under the padding box whenever one of those modes is selected. ECB and CBC are unaffected, since both genuinely need padding and PKCS#7 is the shared default everywhere.
Matching this page with OpenSSL and PHP
Pick hex key mode, AES with a 32 byte key, CBC, PKCS#7, and Base64 output. The result decrypts on a shell with:
echo 'PASTE_BASE64' | openssl enc -d -aes-256-cbc -a -K <hex-key> -iv <hex-iv>In PHP the matching call takes the key as raw bytes, so convert the hex first:
$plain = openssl_decrypt($base64,'aes-256-cbc',hex2bin($hexKey),0,hex2bin($hexIv));The 0 flag matters. Leaving it out is fine, but passing OPENSSL_RAW_DATA tells PHP the input is raw bytes rather than base64, and mixing that up returns false with no error message worth reading.
For passphrase mode the shell equivalent needs the legacy derivation turned back on, since OpenSSL 3 defaults to PBKDF2:
echo 'PASTE_BASE64' | openssl enc -d -aes-256-cbc -a -md md5 -pass pass:YOUR_PASSPHRASEOpenSSL prints a warning about the deprecated derivation. That warning is correct and worth taking seriously, which brings us to the limits of this page.
Do not protect real secrets with this
EVP_BytesToKey runs a single pass of MD5 over the passphrase and salt. Modern derivations run tens of thousands of rounds of a slow hash for exactly this reason, and a weak passphrase falls to a guessing attack in seconds against a single MD5 round. Passphrase mode exists to read and write a legacy format, not to secure a password.
None of the modes here authenticate the ciphertext either. AES-CBC hides content but does not detect tampering, and an attacker who flips bits in the ciphertext produces predictable changes in the decrypted output. Real systems pair encryption with AES-GCM or an HMAC over the ciphertext. GCM is not implemented in the library behind this page, so it is not offered.
Treat this as a workbench for understanding, debugging, and interoperating. For storing customer data, use the crypto library your platform ships, keys from a key management service, and AES-GCM.
Choosing between the four algorithms
- AES at 128 or 256 bits is the answer for anything new. Hardware acceleration lives in every current CPU, and no practical break exists against the cipher itself.
- DES has a 56 bit key. It was broken by brute force in 1998 and falls to rented cloud hardware today. Keep it here only for decrypting archives from systems built before 2005.
- 3DES chains DES three times for an effective 112 bits, and its 64 bit block size is the real problem. NIST withdrew approval for new applications after 2023, and the Sweet32 attack becomes practical once a single key protects more than a few gigabytes. Payment and banking systems still hold plenty of it.
- Rabbit is a stream cipher from the eSTREAM portfolio with a 128 bit key and a 64 bit IV. It has no mode and no padding, so the toggles above hide themselves when you select it. Support outside JavaScript libraries is thin, which makes it a poor pick for anything that has to interoperate.
Where this tool stops
- Text onlyFiles are not accepted. Encrypting a 40 MB archive in a browser tab means holding it twice in memory and base64 growing it by a third. Use
openssl enc or gpg on the file directly. - No GCMAuthenticated encryption is missing because CryptoJS never implemented it. If you need a tag alongside your ciphertext, the Web Crypto API in the same browser does AES-GCM natively.
- Symmetric onlyOne key both encrypts and decrypts. RSA, ECC, and key exchange are outside what this page does, and generating a real key pair in a browser is not something to trust for production keys.
- UTF-8 outputDecryption returns text. Feed it ciphertext that decrypts to binary and the result is empty rather than mangled bytes, which reads as a wrong key even when the key was right.
- No key storageNothing persists. Reload the page and the key field is empty, by design. Losing a key means losing the ciphertext, so write it down before you close the tab.
Everything runs in your browser through the CryptoJS library loaded with the page. No text, key, or result is sent to a server, which you can confirm by opening the network panel and pressing Encrypt. That said, a secret typed into any browser tab has touched your clipboard and possibly a form autofill store, so rotate anything sensitive you paste here.
