Ciphers

XOR

Repeating-key XOR. Vigenère on bytes, with a hex key of any length. UTF-8 text in and hex out, or hex on both sides.
IDxor44 / 44stream · polyalphabetic
Cipher / Polyalphabetic

XOR

Vigenère on bytes, a short key repeated and XORed in.

Keyspace
256^n keys for an n-byte key
Decode
same options back
Works on
UTF-8 bytes, hex out
Family
6 in polyalphabetic
required 1 / 2

Options

keystring
required
Any nonzero even number of hex digits, a key of whole bytes
bytesstring
default text
What the plain side is: text for UTF-8 text, or hex to read and write hex both ways, for bytes that are not text

Access

Createcreate("xor")
CLIciphers encode xor 'Attack at dawn' --key 494345
Tryplayground with the sample above
Kinvigenere, beaufort, autokey, trithemius +1

The smallest cipher in the package. Take the key bytes, repeat them under the text, XOR each pair. Done. Decrypting is the same XOR again, because XOR undoes itself.

It's older than computers, too. Gilbert Vernam built it into AT&T teleprinters in 1917 and patented it in 1919, US 1,310,719. The key sat on punched tape, and the machine XORed its pulses into the Baudot code of the message. Make that key random, as long as the message, and use it once? That's the one-time pad. Repeat a short key instead and you get this page.

Vigenère, but bytes

Vigenère adds a repeating keyword to letters, mod 26. This adds a repeating key to bytes, mod 2 per bit. Same idea, same weakness. That's why its family here is polyalphabetic, next to Vigenère and Beaufort, even though it lives with the stream ciphers.

Cryptopals set 1, challenge 5, is the classic vector. Key ICE, as hex 494345:

ts
const xor = create("xor");
const key = "494345";
const text = "Burning 'em, if you ain't quick and nimble\nI go crazy when I hear a cymbal";
const ciphertext = xor.encode(text, { key }).text; // "0b3637272a2b2e63622c2e69692a2369..."
xor.decode(ciphertext, { key }).text === text; // true

A key of one byte is the single-byte XOR every CTF opens with. A key of 20 just flips the case of letters. hi comes out as 4849, which is HI in ASCII. Cute, not secret.

Keys and bytes

The key is hex, any whole number of bytes, at least one. Case doesn't matter and spaces are ignored. There's no upper limit in the library. A key longer than the text just leaves its tail unused. The agent tools cap it at 1000 characters, like every other key.

By default text goes in as UTF-8 and ciphertext comes out as lowercase hex, same as RC4 and Rabbit. decode wants that hex back and fails with Decrypted bytes are not UTF-8 text; pass bytes: hex to get them as hex when the result isn't text.

And puzzles love results that aren't text. A layer of XOR over another cipher, a key hidden as raw bytes, a blob that's half binary. So bytes: "hex" reads hex and writes hex, in both directions:

ts
xor.decode("00ff80", { key: "ff", bytes: "hex" }).text; // "ff007f"

No UTF-8 check there. You get the bytes, and you decide what they are. On the CLI it's --bytes hex.

Known text gives the key

Here's the fun part. XOR the ciphertext with text you know, and the key falls out. Guess the Cryptopals message starts with Burning and use that as the key:

ts
xor.decode("0b3637272a2b2e63", { key: "4275726e696e6720", bytes: "hex" }).text;
// "4943454943454943", which is "ICEICEIC"

ICE, over and over. That's the whole key, and its length too.

Same key twice is worse. XOR two ciphertexts and the key cancels out, no guessing needed:

ts
xor.encode("Attack at dawn", { key }).text; // "08373128202e6922316927243e2d"
xor.encode("Attack at dusk", { key }).text; // "08373128202e6922316927303a28"
// XOR: "0000000000000000000000140405"

That's dawn XOR dusk, the same bytes RC4 gave. Every stream cipher with a reused keystream ends here. This one just gets here on purpose.

Checked against

The Cryptopals 1.5 vector, both ways and both byte forms. The rest is arithmetic you can check with a pencil, like 00ff80 under ff giving ff007f.

Errors

A key that isn't hex, or isn't whole bytes, is an InvalidOptionError. A missing key is a MissingOptionError, and so is an empty one. A bytes other than text or hex is an InvalidOptionError too. On decode, an odd number of hex digits or a letter past f is a CipherError.

No integrity check, no secrecy worth the name. A puzzle hands you hex and a short word? Try it here before anything fancier.