diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 4adaa5e8..43c23c62 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -134,7 +134,8 @@ "Typex", "Lorenz", "Colossus", - "SIGABA" + "SIGABA", + "Rabbit Stream Cipher" ] }, { diff --git a/src/core/operations/RabbitStreamCipher.mjs b/src/core/operations/RabbitStreamCipher.mjs new file mode 100644 index 00000000..7d030a6f --- /dev/null +++ b/src/core/operations/RabbitStreamCipher.mjs @@ -0,0 +1,247 @@ +/** + * @author mikecat + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import Utils from "../Utils.mjs"; +import { toHexFast } from "../lib/Hex.mjs"; +import OperationError from "../errors/OperationError.mjs"; + +/** + * Rabbit Stream Cipher operation + */ +class RabbitStreamCipher extends Operation { + + /** + * RabbitStreamCipher constructor + */ + constructor() { + super(); + + this.name = "Rabbit Stream Cipher"; + this.module = "Ciphers"; + this.description = "Rabbit Stream Cipher, a stream cipher algorithm defined in RFC4503.

The cipher uses a 128-bit key and an optional 64-bit initialization vector (IV).

big-endian: based on RFC4503 and RFC3447
little-endian: compatible with Crypto++"; + this.infoURL = "https://wikipedia.org/wiki/Rabbit_(cipher)"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Key", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "IV", + "type": "toggleString", + "value": "", + "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"] + }, + { + "name": "Endianness", + "type": "option", + "value": ["Big", "Little"] + }, + { + "name": "Input", + "type": "option", + "value": ["Raw", "Hex"] + }, + { + "name": "Output", + "type": "option", + "value": ["Raw", "Hex"] + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const key = Utils.convertToByteArray(args[0].string, args[0].option), + iv = Utils.convertToByteArray(args[1].string, args[1].option), + endianness = args[2], + inputType = args[3], + outputType = args[4]; + + const littleEndian = endianness === "Little"; + + if (key.length !== 16) { + throw new OperationError(`Invalid key length: ${key.length} bytes (expected: 16)`); + } + if (iv.length !== 0 && iv.length !== 8) { + throw new OperationError(`Invalid IV length: ${iv.length} bytes (expected: 0 or 8)`); + } + + // Inner State + const X = new Uint32Array(8), C = new Uint32Array(8); + let b = 0; + + // Counter System + const A = [ + 0x4d34d34d, 0xd34d34d3, 0x34d34d34, 0x4d34d34d, + 0xd34d34d3, 0x34d34d34, 0x4d34d34d, 0xd34d34d3 + ]; + const counterUpdate = function() { + for (let j = 0; j < 8; j++) { + const temp = C[j] + A[j] + b; + b = (temp / ((1 << 30) * 4)) >>> 0; + C[j] = temp; + } + }; + + // Next-State Function + const g = function(u, v) { + const uv = (u + v) >>> 0; + const upper = uv >>> 16, lower = uv & 0xffff; + const upperUpper = upper * upper; + const upperLower2 = 2 * upper * lower; + const lowerLower = lower * lower; + const mswTemp = upperUpper + ((upperLower2 / (1 << 16)) >>> 0); + const lswTemp = lowerLower + (upperLower2 & 0xffff) * (1 << 16); + const msw = mswTemp + ((lswTemp / ((1 << 30) * 4)) >>> 0); + const lsw = lswTemp >>> 0; + return lsw ^ msw; + }; + const leftRotate = function(value, width) { + return (value << width) | (value >>> (32 - width)); + }; + const nextStateHelper1 = function(v0, v1, v2) { + return v0 + leftRotate(v1, 16) + leftRotate(v2, 16); + }; + const nextStateHelper2 = function(v0, v1, v2) { + return v0 + leftRotate(v1, 8) + v2; + }; + const G = new Uint32Array(8); + const nextState = function() { + for (let j = 0; j < 8; j++) { + G[j] = g(X[j], C[j]); + } + X[0] = nextStateHelper1(G[0], G[7], G[6]); + X[1] = nextStateHelper2(G[1], G[0], G[7]); + X[2] = nextStateHelper1(G[2], G[1], G[0]); + X[3] = nextStateHelper2(G[3], G[2], G[1]); + X[4] = nextStateHelper1(G[4], G[3], G[2]); + X[5] = nextStateHelper2(G[5], G[4], G[3]); + X[6] = nextStateHelper1(G[6], G[5], G[4]); + X[7] = nextStateHelper2(G[7], G[6], G[5]); + }; + + // Key Setup Scheme + const K = new Uint16Array(8); + if (littleEndian) { + for (let i = 0; i < 8; i++) { + K[i] = (key[1 + 2 * i] << 8) | key[2 * i]; + } + } else { + for (let i = 0; i < 8; i++) { + K[i] = (key[14 - 2 * i] << 8) | key[15 - 2 * i]; + } + } + for (let j = 0; j < 8; j++) { + if (j % 2 === 0) { + X[j] = (K[(j + 1) % 8] << 16) | K[j]; + C[j] = (K[(j + 4) % 8] << 16) | K[(j + 5) % 8]; + } else { + X[j] = (K[(j + 5) % 8] << 16) | K[(j + 4) % 8]; + C[j] = (K[j] << 16) | K[(j + 1) % 8]; + } + } + for (let i = 0; i < 4; i++) { + counterUpdate(); + nextState(); + } + for (let j = 0; j < 8; j++) { + C[j] = C[j] ^ X[(j + 4) % 8]; + } + + // IV Setup Scheme + if (iv.length === 8) { + const getIVValue = function(a, b, c, d) { + if (littleEndian) { + return (iv[a] << 24) | (iv[b] << 16) | + (iv[c] << 8) | iv[d]; + } else { + return (iv[7 - a] << 24) | (iv[7 - b] << 16) | + (iv[7 - c] << 8) | iv[7 - d]; + } + }; + C[0] = C[0] ^ getIVValue(3, 2, 1, 0); + C[1] = C[1] ^ getIVValue(7, 6, 3, 2); + C[2] = C[2] ^ getIVValue(7, 6, 5, 4); + C[3] = C[3] ^ getIVValue(5, 4, 1, 0); + C[4] = C[4] ^ getIVValue(3, 2, 1, 0); + C[5] = C[5] ^ getIVValue(7, 6, 3, 2); + C[6] = C[6] ^ getIVValue(7, 6, 5, 4); + C[7] = C[7] ^ getIVValue(5, 4, 1, 0); + for (let i = 0; i < 4; i++) { + counterUpdate(); + nextState(); + } + } + + // Extraction Scheme + const S = new Uint8Array(16); + const extract = function() { + let pos = 0; + const addPart = function(value) { + S[pos++] = value >>> 8; + S[pos++] = value & 0xff; + }; + counterUpdate(); + nextState(); + addPart((X[6] >>> 16) ^ (X[1] & 0xffff)); + addPart((X[6] & 0xffff) ^ (X[3] >>> 16)); + addPart((X[4] >>> 16) ^ (X[7] & 0xffff)); + addPart((X[4] & 0xffff) ^ (X[1] >>> 16)); + addPart((X[2] >>> 16) ^ (X[5] & 0xffff)); + addPart((X[2] & 0xffff) ^ (X[7] >>> 16)); + addPart((X[0] >>> 16) ^ (X[3] & 0xffff)); + addPart((X[0] & 0xffff) ^ (X[5] >>> 16)); + if (littleEndian) { + for (let i = 0, j = S.length - 1; i < j;) { + const temp = S[i]; + S[i] = S[j]; + S[j] = temp; + i++; + j--; + } + } + }; + + const data = Utils.convertToByteString(input, inputType); + const result = new Uint8Array(data.length); + for (let i = 0; i <= data.length - 16; i += 16) { + extract(); + for (let j = 0; j < 16; j++) { + result[i + j] = data.charCodeAt(i + j) ^ S[j]; + } + } + if (data.length % 16 !== 0) { + const offset = data.length - data.length % 16; + const length = data.length - offset; + extract(); + if (littleEndian) { + for (let j = 0; j < length; j++) { + result[offset + j] = data.charCodeAt(offset + j) ^ S[j]; + } + } else { + for (let j = 0; j < length; j++) { + result[offset + j] = data.charCodeAt(offset + j) ^ S[16 - length + j]; + } + } + } + if (outputType === "Hex") { + return toHexFast(result); + } + return Utils.byteArrayToChars(result); + } + +} + +export default RabbitStreamCipher; diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 885ba919..cb410915 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -129,6 +129,7 @@ import "./tests/Shuffle.mjs"; import "./tests/FletcherChecksum.mjs"; import "./tests/CMAC.mjs"; import "./tests/AESKeyWrap.mjs"; +import "./tests/RabbitStreamCipher.mjs"; // Cannot test operations that use the File type yet // import "./tests/SplitColourChannels.mjs"; diff --git a/tests/operations/tests/RabbitStreamCipher.mjs b/tests/operations/tests/RabbitStreamCipher.mjs new file mode 100644 index 00000000..fd22ffbd --- /dev/null +++ b/tests/operations/tests/RabbitStreamCipher.mjs @@ -0,0 +1,177 @@ +/** + * @author mikecat + * @copyright Crown Copyright 2022 + * @license Apache-2.0 + */ + +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Rabbit Stream Cipher: RFC Test vector, without IV 1", + input: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + expectedOutput: "b15754f036a5d6ecf56b45261c4af70288e8d815c59c0c397b696c4789c68aa7f416a1c3700cd451da68d1881673d696", + recipeConfig: [ + { + "op": "Rabbit Stream Cipher", + "args": [ + {"option": "Hex", "string": "00000000000000000000000000000000"}, + {"option": "Hex", "string": ""}, + "Big", "Hex", "Hex" + ] + } + ] + }, + { + name: "Rabbit Stream Cipher: RFC Test vector, without IV 2", + input: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + expectedOutput: "3d2df3c83ef627a1e97fc38487e2519cf576cd61f4405b8896bf53aa8554fc19e5547473fbdb43508ae53b20204d4c5e", + recipeConfig: [ + { + "op": "Rabbit Stream Cipher", + "args": [ + {"option": "Hex", "string": "912813292e3d36fe3bfc62f1dc51c3ac"}, + {"option": "Hex", "string": ""}, + "Big", "Hex", "Hex" + ] + } + ] + }, + { + name: "Rabbit Stream Cipher: RFC Test vector, without IV 3", + input: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + expectedOutput: "0cb10dcda041cdac32eb5cfd02d0609b95fc9fca0f17015a7b7092114cff3ead9649e5de8bfc7f3f924147ad3a947428", + recipeConfig: [ + { + "op": "Rabbit Stream Cipher", + "args": [ + {"option": "Hex", "string": "8395741587e0c733e9e9ab01c09b0043"}, + {"option": "Hex", "string": ""}, + "Big", "Hex", "Hex" + ] + } + ] + }, + { + name: "Rabbit Stream Cipher: RFC Test vector, with IV 1", + input: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + expectedOutput: "c6a7275ef85495d87ccd5d376705b7ed5f29a6ac04f5efd47b8f293270dc4a8d2ade822b29de6c1ee52bdb8a47bf8f66", + recipeConfig: [ + { + "op": "Rabbit Stream Cipher", + "args": [ + {"option": "Hex", "string": "00000000000000000000000000000000"}, + {"option": "Hex", "string": "0000000000000000"}, + "Big", "Hex", "Hex" + ] + } + ] + }, + { + name: "Rabbit Stream Cipher: RFC Test vector, with IV 2", + input: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + expectedOutput: "1fcd4eb9580012e2e0dccc9222017d6da75f4e10d12125017b2499ffed936f2eebc112c393e738392356bdd012029ba7", + recipeConfig: [ + { + "op": "Rabbit Stream Cipher", + "args": [ + {"option": "Hex", "string": "00000000000000000000000000000000"}, + {"option": "Hex", "string": "c373f575c1267e59"}, + "Big", "Hex", "Hex" + ] + } + ] + }, + { + name: "Rabbit Stream Cipher: RFC Test vector, with IV 3", + input: "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + expectedOutput: "445ad8c805858dbf70b6af23a151104d96c8f27947f42c5baeae67c6acc35b039fcbfc895fa71c17313df034f01551cb", + recipeConfig: [ + { + "op": "Rabbit Stream Cipher", + "args": [ + {"option": "Hex", "string": "00000000000000000000000000000000"}, + {"option": "Hex", "string": "a6eb561ad2f41727"}, + "Big", "Hex", "Hex" + ] + } + ] + }, + { + name: "Rabbit Stream Cipher: generated stream should be XORed with the input", + input: "cedda96c054e3ddd93da7ed05e2a4b7bdb0c00fe214f03502e2708b2c2bfc77aa2311b0b9af8aa78d119f92b26db0a6b", + expectedOutput: "7f8afd9c33ebeb3166b13bf64260bc7953e4d8ebe4d30f69554e64f54b794ddd5627bac8eaf47e290b7128a330a8dcfd", + recipeConfig: [ + { + "op": "Rabbit Stream Cipher", + "args": [ + {"option": "Hex", "string": "00000000000000000000000000000000"}, + {"option": "Hex", "string": ""}, + "Big", "Hex", "Hex" + ] + } + ] + }, + { + name: "Rabbit Stream Cipher: least significant bits should be used for the last block", + input: "0000000000000000", + expectedOutput: "f56b45261c4af702", + recipeConfig: [ + { + "op": "Rabbit Stream Cipher", + "args": [ + {"option": "Hex", "string": "00000000000000000000000000000000"}, + {"option": "Hex", "string": ""}, + "Big", "Hex", "Hex" + ] + } + ] + }, + { + name: "Rabbit Stream Cipher: invalid key length", + input: "", + expectedOutput: "Invalid key length: 8 bytes (expected: 16)", + recipeConfig: [ + { + "op": "Rabbit Stream Cipher", + "args": [ + {"option": "Hex", "string": "0000000000000000"}, + {"option": "Hex", "string": ""}, + "Big", "Hex", "Hex" + ] + } + ] + }, + { + name: "Rabbit Stream Cipher: invalid IV length", + input: "", + expectedOutput: "Invalid IV length: 4 bytes (expected: 0 or 8)", + recipeConfig: [ + { + "op": "Rabbit Stream Cipher", + "args": [ + {"option": "Hex", "string": "00000000000000000000000000000000"}, + {"option": "Hex", "string": "00000000"}, + "Big", "Hex", "Hex" + ] + } + ] + }, + { + // this testcase is taken from the first example on Crypto++ Wiki + // https://www.cryptopp.com/wiki/Rabbit + name: "Rabbit Stream Cipher: little-endian mode (Crypto++ compatible)", + input: "Rabbit stream cipher test", + expectedOutput: "1ae2d4edcf9b6063b00fd6fda0b223aded157e77031cf0440b", + recipeConfig: [ + { + "op": "Rabbit Stream Cipher", + "args": [ + {"option": "Hex", "string": "23c2731e8b5469fd8dabb5bc592a0f3a"}, + {"option": "Hex", "string": "712906405ef03201"}, + "Little", "Raw", "Hex" + ] + } + ] + }, +]);