diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 43d5dc4e..430f6196 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -131,7 +131,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..b106cb4d
--- /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 = [], C = [];
+ 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 >>> 0;
+ }
+ };
+
+ // 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) >>> 0;
+ };
+ 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)) >>> 0;
+ };
+ const nextStateHelper2 = function(v0, v1, v2) {
+ return (v0 + leftRotate(v1, 8) + v2) >>> 0;
+ };
+ const G = new Array(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 = [];
+ if (littleEndian) {
+ for (let i = 0; i < 8; i++) {
+ K.push((key[1 + 2 * i] << 8) | key[2 * i]);
+ }
+ } else {
+ for (let i = 0; i < 8; i++) {
+ K.push((key[14 - 2 * i] << 8) | key[15 - 2 * i]);
+ }
+ }
+ for (let j = 0; j < 8; j++) {
+ if (j % 2 === 0) {
+ X.push(((K[(j + 1) % 8] << 16) | K[j]) >>> 0);
+ C.push(((K[(j + 4) % 8] << 16) | K[(j + 5) % 8]) >>> 0);
+ } else {
+ X.push(((K[(j + 5) % 8] << 16) | K[(j + 4) % 8]) >>> 0);
+ C.push(((K[j] << 16) | K[(j + 1) % 8]) >>> 0);
+ }
+ }
+ for (let i = 0; i < 4; i++) {
+ counterUpdate();
+ nextState();
+ }
+ for (let j = 0; j < 8; j++) {
+ C[j] = (C[j] ^ X[(j + 4) % 8]) >>> 0;
+ }
+
+ // 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]) >>> 0;
+ } else {
+ return ((iv[7 - a] << 24) | (iv[7 - b] << 16) |
+ (iv[7 - c] << 8) | iv[7 - d]) >>> 0;
+ }
+ };
+ C[0] = (C[0] ^ getIVValue(3, 2, 1, 0)) >>> 0;
+ C[1] = (C[1] ^ getIVValue(7, 6, 3, 2)) >>> 0;
+ C[2] = (C[2] ^ getIVValue(7, 6, 5, 4)) >>> 0;
+ C[3] = (C[3] ^ getIVValue(5, 4, 1, 0)) >>> 0;
+ C[4] = (C[4] ^ getIVValue(3, 2, 1, 0)) >>> 0;
+ C[5] = (C[5] ^ getIVValue(7, 6, 3, 2)) >>> 0;
+ C[6] = (C[6] ^ getIVValue(7, 6, 5, 4)) >>> 0;
+ C[7] = (C[7] ^ getIVValue(5, 4, 1, 0)) >>> 0;
+ for (let i = 0; i < 4; i++) {
+ counterUpdate();
+ nextState();
+ }
+ }
+
+ // Extraction Scheme
+ const S = new Array(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 19e70970..2d303dcb 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -124,6 +124,7 @@ import "./tests/UnescapeString.mjs";
import "./tests/LS47.mjs";
import "./tests/LZString.mjs";
import "./tests/NTLM.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"
+ ]
+ }
+ ]
+ },
+]);