UNPKG

cyberchef

Version:

The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.

626 lines (543 loc) 20.3 kB
/** * Complete implementation of RC6 block cipher encryption/decryption with * configurable word size (w), rounds (r), and key length (b). * * RC6 was an AES finalist designed by Ron Rivest, Matt Robshaw, Ray Sidney, and Yiqun Lisa Yin. * Reference: https://en.wikipedia.org/wiki/RC6 * Test Vectors: https://datatracker.ietf.org/doc/html/draft-krovetz-rc6-rc5-vectors-00 * * The P and Q constants are derived from mathematical constants e (Euler's number) and * φ (golden ratio) as specified in the IETF draft. Master 256-bit values are scaled to * any word size. * * @author Medjedtxm * @copyright Crown Copyright 2026 * @license Apache-2.0 */ import OperationError from "../errors/OperationError.mjs"; /** * Master P constant (256-bit) from IETF draft-krovetz-rc6-rc5-vectors-00 * Derived from Odd((e-2) * 2^256) where e = 2.71828... */ const P_256 = 0xb7e151628aed2a6abf7158809cf4f3c762e7160f38b4da56a784d9045190cfefn; /** * Master Q constant (256-bit) from IETF draft-krovetz-rc6-rc5-vectors-00 * Derived from Odd((φ-1) * 2^256) where φ = 1.61803... (golden ratio) */ const Q_256 = 0x9e3779b97f4a7c15f39cc0605cedc8341082276bf3a27251f86c6a11d0c18e95n; /** * Get P constant for given word size by scaling the 256-bit master constant * @param {number} w - Word size in bits * @returns {bigint} - P constant for word size w */ function getP(w) { return (P_256 >> BigInt(256 - w)) | 1n; // Ensure odd } /** * Get Q constant for given word size by scaling the 256-bit master constant * @param {number} w - Word size in bits * @returns {bigint} - Q constant for word size w */ function getQ(w) { return (Q_256 >> BigInt(256 - w)) | 1n; // Ensure odd } /** * Get block size in bytes for given word size * Block size = 4 words = 4 * (w/8) bytes * @param {number} w - Word size in bits * @returns {number} - Block size in bytes */ export function getBlockSize(w) { return 4 * (w / 8); } /** * Get recommended number of rounds for given word size * @param {number} w - Word size in bits * @returns {number} - Recommended rounds */ export function getDefaultRounds(w) { if (w <= 16) return 16; if (w <= 32) return 20; if (w <= 64) return 24; return 28; } /** * Create mask for w-bit word * @param {number} w - Word size in bits * @returns {bigint} - Mask with w bits set */ function wordMask(w) { return (1n << BigInt(w)) - 1n; } /** * Rotate left for arbitrary word size using BigInt * Uses lower lg(w) bits of n for rotation amount (RC6 spec) * @param {bigint} x - Value to rotate * @param {bigint} n - Rotation amount * @param {number} w - Word size in bits * @param {bigint} lgMask - Mask for lower lg(w) bits * @returns {bigint} - Rotated value */ function ROL(x, n, w, lgMask) { const mask = wordMask(w); // Mask to lg(w) bits, then mod w for non-power-of-2 word sizes // For power-of-2, (n & lgMask) < w always, so mod w is no-op const shift = (n & lgMask) % BigInt(w); return ((x << shift) | (x >> (BigInt(w) - shift))) & mask; } /** * Rotate right for arbitrary word size using BigInt * Uses lower lg(w) bits of n for rotation amount (RC6 spec) * @param {bigint} x - Value to rotate * @param {bigint} n - Rotation amount * @param {number} w - Word size in bits * @param {bigint} lgMask - Mask for lower lg(w) bits * @returns {bigint} - Rotated value */ function ROR(x, n, w, lgMask) { const mask = wordMask(w); // Mask to lg(w) bits, then mod w for non-power-of-2 word sizes // For power-of-2, (n & lgMask) < w always, so mod w is no-op const shift = (n & lgMask) % BigInt(w); return ((x >> shift) | (x << (BigInt(w) - shift))) & mask; } /** * Convert byte array to word array (little-endian) using BigInt * @param {number[]} bytes - Input byte array * @param {number} w - Word size in bits * @returns {bigint[]} - Array of w-bit words as BigInt */ function bytesToWords(bytes, w) { const bytesPerWord = w / 8; const words = []; for (let i = 0; i < bytes.length; i += bytesPerWord) { let word = 0n; for (let j = 0; j < bytesPerWord && (i + j) < bytes.length; j++) { word |= BigInt(bytes[i + j] || 0) << BigInt(j * 8); } words.push(word); } return words; } /** * Convert word array to byte array (little-endian) using BigInt * @param {bigint[]} words - Array of words * @param {number} w - Word size in bits * @returns {number[]} - Output byte array */ function wordsToBytes(words, w) { const bytesPerWord = w / 8; const bytes = []; for (const word of words) { for (let j = 0; j < bytesPerWord; j++) { bytes.push(Number((word >> BigInt(j * 8)) & 0xFFn)); } } return bytes; } /** * Generate round subkeys from user key * * @param {number[]} key - User key as byte array * @param {number} rounds - Number of rounds * @param {number} w - Word size in bits * @returns {bigint[]} - Array of 2r+4 subkeys as BigInt */ function generateSubkeys(key, rounds, w) { const bytesPerWord = w / 8; const b = key.length; const c = Math.max(Math.ceil(b / bytesPerWord), 1); // Convert key bytes to words, pad with zeros if needed const paddedKey = [...key]; while (paddedKey.length < c * bytesPerWord) { paddedKey.push(0); } const L = bytesToWords(paddedKey, w); // Number of subkeys: 2*r + 4 const t = 2 * rounds + 4; // Get P and Q for this word size const P = getP(w); const Q = getQ(w); const mask = wordMask(w); // lg(w) mask for rotation amounts (floor of log2(w), per RC6 spec) const lgw = Math.floor(Math.log2(w)); const lgMask = (1n << BigInt(lgw)) - 1n; // Initialise S array with magic constants const S = new Array(t); S[0] = P; for (let i = 1; i < t; i++) { S[i] = (S[i - 1] + Q) & mask; } // Mix key into S let A = 0n, B = 0n; let i = 0, j = 0; const v = 3 * Math.max(c, t); for (let s = 0; s < v; s++) { A = S[i] = ROL((S[i] + A + B) & mask, 3n, w, lgMask); B = L[j] = ROL((L[j] + A + B) & mask, A + B, w, lgMask); i = (i + 1) % t; j = (j + 1) % c; } return S; } /** * Encrypt a single block using RC6 * * @param {number[]} block - Plaintext block (4*w/8 bytes) * @param {bigint[]} S - Subkeys array * @param {number} rounds - Number of rounds * @param {number} w - Word size in bits * @returns {number[]} - Ciphertext block */ function encryptBlock(block, S, rounds, w) { const mask = wordMask(w); const lgw = BigInt(Math.floor(Math.log2(w))); const lgMask = (1n << lgw) - 1n; // Convert block to 4 words (A, B, C, D) let [A, B, C, D] = bytesToWords(block, w); // Pre-whitening B = (B + S[0]) & mask; D = (D + S[1]) & mask; // Main rounds for (let i = 1; i <= rounds; i++) { // t = ROL(B * (2B + 1), lg(w)) const t = ROL((B * ((2n * B + 1n) & mask)) & mask, lgw, w, lgMask); // u = ROL(D * (2D + 1), lg(w)) const u = ROL((D * ((2n * D + 1n) & mask)) & mask, lgw, w, lgMask); // A = ROL(A ^ t, u) + S[2i] A = (ROL(A ^ t, u, w, lgMask) + S[2 * i]) & mask; // C = ROL(C ^ u, t) + S[2i + 1] C = (ROL(C ^ u, t, w, lgMask) + S[2 * i + 1]) & mask; // Rotate registers: (A, B, C, D) = (B, C, D, A) const temp = A; A = B; B = C; C = D; D = temp; } // Post-whitening A = (A + S[2 * rounds + 2]) & mask; C = (C + S[2 * rounds + 3]) & mask; // Convert words back to bytes return wordsToBytes([A, B, C, D], w); } /** * Decrypt a single block using RC6 * * @param {number[]} block - Ciphertext block (4*w/8 bytes) * @param {bigint[]} S - Subkeys array * @param {number} rounds - Number of rounds * @param {number} w - Word size in bits * @returns {number[]} - Plaintext block */ function decryptBlock(block, S, rounds, w) { const mask = wordMask(w); const lgw = BigInt(Math.floor(Math.log2(w))); const lgMask = (1n << lgw) - 1n; // Convert block to 4 words (A, B, C, D) let [A, B, C, D] = bytesToWords(block, w); // Reverse post-whitening C = (C - S[2 * rounds + 3] + (1n << BigInt(w))) & mask; A = (A - S[2 * rounds + 2] + (1n << BigInt(w))) & mask; // Main rounds in reverse for (let i = rounds; i >= 1; i--) { // Reverse rotate registers: (A, B, C, D) = (D, A, B, C) const temp = D; D = C; C = B; B = A; A = temp; // u = ROL(D * (2D + 1), lg(w)) const u = ROL((D * ((2n * D + 1n) & mask)) & mask, lgw, w, lgMask); // t = ROL(B * (2B + 1), lg(w)) const t = ROL((B * ((2n * B + 1n) & mask)) & mask, lgw, w, lgMask); // C = ROR(C - S[2i + 1], t) ^ u C = ROR((C - S[2 * i + 1] + (1n << BigInt(w))) & mask, t, w, lgMask) ^ u; // A = ROR(A - S[2i], u) ^ t A = ROR((A - S[2 * i] + (1n << BigInt(w))) & mask, u, w, lgMask) ^ t; } // Reverse pre-whitening D = (D - S[1] + (1n << BigInt(w))) & mask; B = (B - S[0] + (1n << BigInt(w))) & mask; // Convert words back to bytes return wordsToBytes([A, B, C, D], w); } /** * XOR two blocks * @param {number[]} a - First block * @param {number[]} b - Second block * @returns {number[]} - XOR result */ function xorBlocks(a, b) { const result = new Array(a.length); for (let i = 0; i < a.length; i++) { result[i] = a[i] ^ b[i]; } return result; } /** * Increment counter (little-endian) * @param {number[]} counter - Counter block * @returns {number[]} - Incremented counter */ function incrementCounter(counter) { const result = [...counter]; for (let i = 0; i < result.length; i++) { result[i]++; if (result[i] <= 255) break; result[i] = 0; } return result; } /** * Apply padding to message * @param {number[]} message - Original message * @param {string} padding - Padding type ("NO", "PKCS5", "ZERO", "RANDOM", "BIT") * @param {number} blockSize - Block size in bytes * @returns {number[]} - Padded message */ function applyPadding(message, padding, blockSize) { const remainder = message.length % blockSize; let nPadding = remainder === 0 ? 0 : blockSize - remainder; // For PKCS5, always add at least one byte (full block if already aligned) if (padding === "PKCS5" && remainder === 0) { nPadding = blockSize; } if (nPadding === 0) return [...message]; const paddedMessage = [...message]; switch (padding) { case "NO": throw new OperationError(`No padding requested but input is not a ${blockSize}-byte multiple.`); case "PKCS5": for (let i = 0; i < nPadding; i++) { paddedMessage.push(nPadding); } break; case "ZERO": for (let i = 0; i < nPadding; i++) { paddedMessage.push(0); } break; case "RANDOM": for (let i = 0; i < nPadding; i++) { paddedMessage.push(Math.floor(Math.random() * 256)); } break; case "BIT": paddedMessage.push(0x80); for (let i = 1; i < nPadding; i++) { paddedMessage.push(0); } break; default: throw new OperationError(`Unknown padding type: ${padding}`); } return paddedMessage; } /** * Remove padding from message * @param {number[]} message - Padded message * @param {string} padding - Padding type ("NO", "PKCS5", "ZERO", "RANDOM", "BIT") * @param {number} blockSize - Block size in bytes * @returns {number[]} - Unpadded message */ function removePadding(message, padding, blockSize) { if (message.length === 0) return message; switch (padding) { case "NO": case "ZERO": case "RANDOM": // These padding types cannot be reliably removed return message; case "PKCS5": { const padByte = message[message.length - 1]; if (padByte > 0 && padByte <= blockSize) { // Verify padding for (let i = 0; i < padByte; i++) { if (message[message.length - 1 - i] !== padByte) { throw new OperationError("Invalid PKCS#5 padding."); } } return message.slice(0, message.length - padByte); } throw new OperationError("Invalid PKCS#5 padding."); } case "BIT": { // Find 0x80 byte working backwards, skipping zeros for (let i = message.length - 1; i >= 0; i--) { if (message[i] === 0x80) { return message.slice(0, i); } else if (message[i] !== 0) { throw new OperationError("Invalid BIT padding."); } } throw new OperationError("Invalid BIT padding."); } default: throw new OperationError(`Unknown padding type: ${padding}`); } } /** * Encrypt using RC6 cipher with specified block mode * * @param {number[]} message - Plaintext as byte array * @param {number[]} key - Key as byte array * @param {number[]} iv - IV (block size bytes, not used for ECB) * @param {string} mode - Block cipher mode ("ECB", "CBC", "CFB", "OFB", "CTR") * @param {string} padding - Padding type ("NO", "PKCS5", "ZERO", "RANDOM", "BIT") * @param {number} rounds - Number of rounds (default: 20) * @param {number} w - Word size in bits (default: 32) * @returns {number[]} - Ciphertext as byte array */ export function encryptRC6(message, key, iv, mode = "ECB", padding = "PKCS5", rounds = 20, w = 32) { const blockSize = getBlockSize(w); const messageLength = message.length; if (messageLength === 0) return []; const S = generateSubkeys(key, rounds, w); // Apply padding for ECB/CBC modes let paddedMessage; if (mode === "ECB" || mode === "CBC") { paddedMessage = applyPadding(message, padding, blockSize); } else { // Stream modes (CFB, OFB, CTR) don't need padding paddedMessage = [...message]; } const cipherText = []; switch (mode) { case "ECB": for (let i = 0; i < paddedMessage.length; i += blockSize) { const block = paddedMessage.slice(i, i + blockSize); cipherText.push(...encryptBlock(block, S, rounds, w)); } break; case "CBC": { let ivBlock = [...iv]; for (let i = 0; i < paddedMessage.length; i += blockSize) { const block = paddedMessage.slice(i, i + blockSize); const xored = xorBlocks(block, ivBlock); ivBlock = encryptBlock(xored, S, rounds, w); cipherText.push(...ivBlock); } break; } case "CFB": { let ivBlock = [...iv]; for (let i = 0; i < paddedMessage.length; i += blockSize) { const encrypted = encryptBlock(ivBlock, S, rounds, w); const block = paddedMessage.slice(i, i + blockSize); // Pad block if shorter than blockSize while (block.length < blockSize) block.push(0); ivBlock = xorBlocks(encrypted, block); cipherText.push(...ivBlock); } return cipherText.slice(0, messageLength); } case "OFB": { let ivBlock = [...iv]; for (let i = 0; i < paddedMessage.length; i += blockSize) { ivBlock = encryptBlock(ivBlock, S, rounds, w); const block = paddedMessage.slice(i, i + blockSize); // Pad block if shorter than blockSize while (block.length < blockSize) block.push(0); cipherText.push(...xorBlocks(ivBlock, block)); } return cipherText.slice(0, messageLength); } case "CTR": { let counter = [...iv]; for (let i = 0; i < paddedMessage.length; i += blockSize) { const encrypted = encryptBlock(counter, S, rounds, w); const block = paddedMessage.slice(i, i + blockSize); // Pad block if shorter than blockSize while (block.length < blockSize) block.push(0); cipherText.push(...xorBlocks(encrypted, block)); counter = incrementCounter(counter); } return cipherText.slice(0, messageLength); } default: throw new OperationError(`Invalid block cipher mode: ${mode}`); } return cipherText; } /** * Decrypt using RC6 cipher with specified block mode * * @param {number[]} cipherText - Ciphertext as byte array * @param {number[]} key - Key as byte array * @param {number[]} iv - IV (block size bytes, not used for ECB) * @param {string} mode - Block cipher mode ("ECB", "CBC", "CFB", "OFB", "CTR") * @param {string} padding - Padding type ("NO", "PKCS5", "ZERO", "RANDOM", "BIT") * @param {number} rounds - Number of rounds (default: 20) * @param {number} w - Word size in bits (default: 32) * @returns {number[]} - Plaintext as byte array */ export function decryptRC6(cipherText, key, iv, mode = "ECB", padding = "PKCS5", rounds = 20, w = 32) { const blockSize = getBlockSize(w); const originalLength = cipherText.length; if (originalLength === 0) return []; const S = generateSubkeys(key, rounds, w); if (mode === "ECB" || mode === "CBC") { if ((originalLength % blockSize) !== 0) throw new OperationError(`Invalid ciphertext length: ${originalLength} bytes. Must be a multiple of ${blockSize}.`); } else { // Pad for stream modes while ((cipherText.length % blockSize) !== 0) cipherText.push(0); } const plainText = []; switch (mode) { case "ECB": for (let i = 0; i < cipherText.length; i += blockSize) { const block = cipherText.slice(i, i + blockSize); plainText.push(...decryptBlock(block, S, rounds, w)); } break; case "CBC": { let ivBlock = [...iv]; for (let i = 0; i < cipherText.length; i += blockSize) { const block = cipherText.slice(i, i + blockSize); const decrypted = decryptBlock(block, S, rounds, w); plainText.push(...xorBlocks(decrypted, ivBlock)); ivBlock = block; } break; } case "CFB": { let ivBlock = [...iv]; for (let i = 0; i < cipherText.length; i += blockSize) { const encrypted = encryptBlock(ivBlock, S, rounds, w); const block = cipherText.slice(i, i + blockSize); plainText.push(...xorBlocks(encrypted, block)); ivBlock = block; } return plainText.slice(0, originalLength); } case "OFB": { let ivBlock = [...iv]; for (let i = 0; i < cipherText.length; i += blockSize) { ivBlock = encryptBlock(ivBlock, S, rounds, w); const block = cipherText.slice(i, i + blockSize); plainText.push(...xorBlocks(ivBlock, block)); } return plainText.slice(0, originalLength); } case "CTR": { let counter = [...iv]; for (let i = 0; i < cipherText.length; i += blockSize) { const encrypted = encryptBlock(counter, S, rounds, w); const block = cipherText.slice(i, i + blockSize); plainText.push(...xorBlocks(encrypted, block)); counter = incrementCounter(counter); } return plainText.slice(0, originalLength); } default: throw new OperationError(`Invalid block cipher mode: ${mode}`); } // Remove padding for ECB/CBC modes if (mode === "ECB" || mode === "CBC") { return removePadding(plainText, padding, blockSize); } return plainText.slice(0, originalLength); }