Ciphers

AES (passphrase)

What CryptoJS.AES.encrypt(message, passphrase) leaves on a page. Base64 starting U2FsdGVkX1, AES-CBC underneath, key and IV squeezed out of the passphrase with MD5.
IDaes-passphrase31 / 43block · substitution-permutation
Cipher / SP network

AES (passphrase)

What CryptoJS leaves on a page, AES keyed by a password.

Keyspace
set by the passphrase, not by keyLength
Decode
same options back
Works on
UTF-8 bytes, base64 out
Family
13 in sp network
required 1 / 4

Options

keystring
required
The passphrase, any text, read as UTF-8
keyLengthnumber
default 256
Key length in bits, 128 to 1024 in steps of 32: CryptoJS keySize times 32. Past 256 it runs AES with keyLength / 32 + 6 rounds, as CryptoJS does
iterationsnumber
default 1
MD5 passes per derived block, 1 to 100000: CryptoJS EvpKDF.cfg.iterations
saltstring
optional
Encoding only: 16 hex digits. Random when left out; decoding reads it from the ciphertext

Access

Createcreate("aes-passphrase")
CLIciphers encode aes-passphrase 'ATTACK AT DAWN' --key secret --salt 0123456789abcdef
Tryplayground with the sample above
Kinaes, aes-cbc, aes-cfb, aes-ofb +8

Seen var msg = "U2FsdGVkX1..." in a page source? That's this one. CryptoJS.AES.encrypt(message, passphrase) writes it, and puzzle pages love it. Every other block cipher here wants a hex key. This one wants a password.

Decode the base64 and the first eight bytes read Salted__. Then eight bytes of random salt, then the ciphertext. That's why every such string opens with the same ten characters. The salt is new on every run, so one message never looks the same twice.

ts
const aes = create("aes-passphrase");
aes.decode("U2FsdGVkX18BI0VniavN73vYVeKsrRmd74V3dwhYQ3E=", { key: "secret" }).text;
// "ATTACK AT DAWN"
aes.encode("ATTACK AT DAWN", { key: "secret", salt: "0123456789abcdef" }).text;
// "U2FsdGVkX18BI0VniavN73vYVeKsrRmd74V3dwhYQ3E="

Where does the key come from?

From MD5, and not much of it. OpenSSL calls the recipe EVP_BytesToKey, CryptoJS calls it EvpKDF. Hash the passphrase with the salt. Hash that result with the passphrase and the salt again. Repeat until there are bytes for the key and the 16-byte IV. For AES-256 that's three MD5 calls. Three. A GPU does billions of those a second, so a weak passphrase falls fast.

The body is plain AES-CBC with PKCS#7 padding. It's the same bytes openssl enc -aes-256-cbc -md md5 gives. OpenSSL switched its default digest to SHA-256 in 1.1.0, so leave out -md md5 and you get something else.

The options

key is the passphrase, any text, read as UTF-8. keyLength and iterations are the two knobs CryptoJS lets a page turn. Most pages leave them alone, so the defaults are 256 bits and one pass.

keyLength is in bits, 128 to 1024 in steps of 32. CryptoJS counts 32-bit words, so CryptoJS.algo.AES.keySize = 32 means keyLength: 1024. Is that still AES? Not really. CryptoJS runs the Rijndael key schedule on whatever it gets and sets the rounds to words plus 6. A 1024-bit key gets 38 rounds. No standard has that. Some puzzle pages do, and so does this cipher.

iterations is EvpKDF.cfg.iterations, 1 to 100000. Each extra pass hashes every derived block once more. The cap keeps one call under about a second.

ts
const wide = { key: "secret", keyLength: 1024, iterations: 10000 };
aes.encode("ATTACK AT DAWN", { ...wide, salt: "0123456789abcdef" }).text;
// "U2FsdGVkX18BI0VniavN7/GNI6WzbZKfYzF61Y37l9k="
aes.decode("U2FsdGVkX18BI0VniavN7/GNI6WzbZKfYzF61Y37l9k=", { key: "secret" });
// CipherError: [aes-passphrase] Decrypted blocks do not end in PKCS#7 padding: wrong passphrase, keyLength or iterations

Right passphrase, wrong knobs, and the padding check catches it. Pass wide and ATTACK AT DAWN comes back.

salt matters only for encode: 16 hex digits, random when you leave it out. decode reads the salt from the ciphertext and ignores the option.

The vectors come from crypto-js 4.2.0. The 128 and 256-bit ones match openssl enc -md md5 too.

A missing key is a MissingOptionError. A keyLength or iterations out of range is an InvalidOptionError. On decode, text that isn't base64, has no Salted__ header, or isn't whole blocks after the salt is a CipherError.

Plain TypeScript, not constant time. Good for opening a puzzle page, useless for locking anything.