diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json
index 83892c36..bad0bdde 100644
--- a/src/core/config/Categories.json
+++ b/src/core/config/Categories.json
@@ -132,7 +132,9 @@
"Typex",
"Lorenz",
"Colossus",
- "SIGABA"
+ "SIGABA",
+ "AES Key Wrap",
+ "AES Key Unwrap"
]
},
{
diff --git a/src/core/operations/AESKeyUnwrap.mjs b/src/core/operations/AESKeyUnwrap.mjs
new file mode 100644
index 00000000..1558847a
--- /dev/null
+++ b/src/core/operations/AESKeyUnwrap.mjs
@@ -0,0 +1,128 @@
+/**
+ * @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 forge from "node-forge";
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * AES Key Unwrap operation
+ */
+class AESKeyUnwrap extends Operation {
+
+ /**
+ * AESKeyUnwrap constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "AES Key Unwrap";
+ this.module = "Ciphers";
+ this.description = "Decryptor for a key wrapping algorithm defined in RFC3394, which is used to protect keys in untrusted storage or communications, using AES.
This algorithm uses an AES key (KEK: key-encryption key) and a 64-bit IV to decrypt 64-bit blocks.";
+ this.infoURL = "https://wikipedia.org/wiki/Key_wrap";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Key (KEK)",
+ "type": "toggleString",
+ "value": "",
+ "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ "name": "IV",
+ "type": "toggleString",
+ "value": "a6a6a6a6a6a6a6a6",
+ "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ "name": "Input",
+ "type": "option",
+ "value": ["Hex", "Raw"]
+ },
+ {
+ "name": "Output",
+ "type": "option",
+ "value": ["Hex", "Raw"]
+ },
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const kek = Utils.convertToByteString(args[0].string, args[0].option),
+ iv = Utils.convertToByteString(args[1].string, args[1].option),
+ inputType = args[2],
+ outputType = args[3];
+
+ if (kek.length !== 16 && kek.length !== 24 && kek.length !== 32) {
+ throw new OperationError("KEK must be either 16, 24, or 32 bytes (currently " + kek.length + " bytes)");
+ }
+ if (iv.length !== 8) {
+ throw new OperationError("IV must be 8 bytes (currently " + iv.length + " bytes)");
+ }
+ const inputData = Utils.convertToByteString(input, inputType);
+ if (inputData.length % 8 !== 0 || inputData.length < 24) {
+ throw new OperationError("input must be 8n (n>=3) bytes (currently " + inputData.length + " bytes)");
+ }
+
+ const cipher = forge.cipher.createCipher("AES-ECB", kek);
+ cipher.start();
+ cipher.update(forge.util.createBuffer(""));
+ cipher.finish();
+ const paddingBlock = cipher.output.getBytes();
+
+ const decipher = forge.cipher.createDecipher("AES-ECB", kek);
+
+ let A = inputData.substring(0, 8);
+ const R = [];
+ for (let i = 8; i < inputData.length; i += 8) {
+ R.push(inputData.substring(i, i + 8));
+ }
+ let cntLower = R.length >>> 0;
+ let cntUpper = (R.length / ((1 << 30) * 4)) >>> 0;
+ cntUpper = cntUpper * 6 + ((cntLower * 6 / ((1 << 30) * 4)) >>> 0);
+ cntLower = cntLower * 6 >>> 0;
+ for (let j = 5; j >= 0; j--) {
+ for (let i = R.length - 1; i >= 0; i--) {
+ const aBuffer = Utils.strToArrayBuffer(A);
+ const aView = new DataView(aBuffer);
+ aView.setUint32(0, aView.getUint32(0) ^ cntUpper);
+ aView.setUint32(4, aView.getUint32(4) ^ cntLower);
+ A = Utils.arrayBufferToStr(aBuffer, false);
+ decipher.start();
+ decipher.update(forge.util.createBuffer(A + R[i] + paddingBlock));
+ decipher.finish();
+ const B = decipher.output.getBytes();
+ A = B.substring(0, 8);
+ R[i] = B.substring(8, 16);
+ cntLower--;
+ if (cntLower < 0) {
+ cntUpper--;
+ cntLower = 0xffffffff;
+ }
+ }
+ }
+ if (A !== iv) {
+ throw new OperationError("IV mismatch");
+ }
+ const P = R.join("");
+
+ if (outputType === "Hex") {
+ return toHexFast(Utils.strToArrayBuffer(P));
+ }
+ return P;
+ }
+
+}
+
+export default AESKeyUnwrap;
diff --git a/src/core/operations/AESKeyWrap.mjs b/src/core/operations/AESKeyWrap.mjs
new file mode 100644
index 00000000..38867156
--- /dev/null
+++ b/src/core/operations/AESKeyWrap.mjs
@@ -0,0 +1,115 @@
+/**
+ * @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 forge from "node-forge";
+import OperationError from "../errors/OperationError.mjs";
+
+/**
+ * AES Key Wrap operation
+ */
+class AESKeyWrap extends Operation {
+
+ /**
+ * AESKeyWrap constructor
+ */
+ constructor() {
+ super();
+
+ this.name = "AES Key Wrap";
+ this.module = "Ciphers";
+ this.description = "A key wrapping algorithm defined in RFC3394, which is used to protect keys in untrusted storage or communications, using AES.
This algorithm uses an AES key (KEK: key-encryption key) and a 64-bit IV to encrypt 64-bit blocks.";
+ this.infoURL = "https://wikipedia.org/wiki/Key_wrap";
+ this.inputType = "string";
+ this.outputType = "string";
+ this.args = [
+ {
+ "name": "Key (KEK)",
+ "type": "toggleString",
+ "value": "",
+ "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ "name": "IV",
+ "type": "toggleString",
+ "value": "a6a6a6a6a6a6a6a6",
+ "toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
+ },
+ {
+ "name": "Input",
+ "type": "option",
+ "value": ["Hex", "Raw"]
+ },
+ {
+ "name": "Output",
+ "type": "option",
+ "value": ["Hex", "Raw"]
+ },
+ ];
+ }
+
+ /**
+ * @param {string} input
+ * @param {Object[]} args
+ * @returns {string}
+ */
+ run(input, args) {
+ const kek = Utils.convertToByteString(args[0].string, args[0].option),
+ iv = Utils.convertToByteString(args[1].string, args[1].option),
+ inputType = args[2],
+ outputType = args[3];
+
+ if (kek.length !== 16 && kek.length !== 24 && kek.length !== 32) {
+ throw new OperationError("KEK must be either 16, 24, or 32 bytes (currently " + kek.length + " bytes)");
+ }
+ if (iv.length !== 8) {
+ throw new OperationError("IV must be 8 bytes (currently " + iv.length + " bytes)");
+ }
+ const inputData = Utils.convertToByteString(input, inputType);
+ if (inputData.length % 8 !== 0 || inputData.length < 16) {
+ throw new OperationError("input must be 8n (n>=2) bytes (currently " + inputData.length + " bytes)");
+ }
+
+ const cipher = forge.cipher.createCipher("AES-ECB", kek);
+
+ let A = iv;
+ const R = [];
+ for (let i = 0; i < inputData.length; i += 8) {
+ R.push(inputData.substring(i, i + 8));
+ }
+ let cntLower = 1, cntUpper = 0;
+ for (let j = 0; j < 6; j++) {
+ for (let i = 0; i < R.length; i++) {
+ cipher.start();
+ cipher.update(forge.util.createBuffer(A + R[i]));
+ cipher.finish();
+ const B = cipher.output.getBytes();
+ const msbBuffer = Utils.strToArrayBuffer(B.substring(0, 8));
+ const msbView = new DataView(msbBuffer);
+ msbView.setUint32(0, msbView.getUint32(0) ^ cntUpper);
+ msbView.setUint32(4, msbView.getUint32(4) ^ cntLower);
+ A = Utils.arrayBufferToStr(msbBuffer, false);
+ R[i] = B.substring(8, 16);
+ cntLower++;
+ if (cntLower > 0xffffffff) {
+ cntUpper++;
+ cntLower = 0;
+ }
+ }
+ }
+ const C = A + R.join("");
+
+ if (outputType === "Hex") {
+ return toHexFast(Utils.strToArrayBuffer(C));
+ }
+ return C;
+ }
+
+}
+
+export default AESKeyWrap;
diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs
index 9afce64e..885ba919 100644
--- a/tests/operations/index.mjs
+++ b/tests/operations/index.mjs
@@ -128,6 +128,7 @@ import "./tests/NTLM.mjs";
import "./tests/Shuffle.mjs";
import "./tests/FletcherChecksum.mjs";
import "./tests/CMAC.mjs";
+import "./tests/AESKeyWrap.mjs";
// Cannot test operations that use the File type yet
// import "./tests/SplitColourChannels.mjs";
diff --git a/tests/operations/tests/AESKeyWrap.mjs b/tests/operations/tests/AESKeyWrap.mjs
new file mode 100644
index 00000000..bca36a40
--- /dev/null
+++ b/tests/operations/tests/AESKeyWrap.mjs
@@ -0,0 +1,324 @@
+/**
+ * @author mikecat
+ * @copyright Crown Copyright 2022
+ * @license Apache-2.0
+ */
+import TestRegister from "../../lib/TestRegister.mjs";
+
+TestRegister.addTests([
+ {
+ "name": "AES Key Wrap: RFC Test Vector, 128-bit data, 128-bit KEK",
+ "input": "00112233445566778899aabbccddeeff",
+ "expectedOutput": "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5",
+ "recipeConfig": [
+ {
+ "op": "AES Key Wrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Wrap: RFC Test Vector, 128-bit data, 192-bit KEK",
+ "input": "00112233445566778899aabbccddeeff",
+ "expectedOutput": "96778b25ae6ca435f92b5b97c050aed2468ab8a17ad84e5d",
+ "recipeConfig": [
+ {
+ "op": "AES Key Wrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f1011121314151617"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Wrap: RFC Test Vector, 128-bit data, 256-bit KEK",
+ "input": "00112233445566778899aabbccddeeff",
+ "expectedOutput": "64e8c3f9ce0f5ba263e9777905818a2a93c8191e7d6e8ae7",
+ "recipeConfig": [
+ {
+ "op": "AES Key Wrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Wrap: RFC Test Vector, 192-bit data, 192-bit KEK",
+ "input": "00112233445566778899aabbccddeeff0001020304050607",
+ "expectedOutput": "031d33264e15d33268f24ec260743edce1c6c7ddee725a936ba814915c6762d2",
+ "recipeConfig": [
+ {
+ "op": "AES Key Wrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f1011121314151617"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Wrap: RFC Test Vector, 192-bit data, 256-bit KEK",
+ "input": "00112233445566778899aabbccddeeff0001020304050607",
+ "expectedOutput": "a8f9bc1612c68b3ff6e6f4fbe30e71e4769c8b80a32cb8958cd5d17d6b254da1",
+ "recipeConfig": [
+ {
+ "op": "AES Key Wrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Wrap: RFC Test Vector, 256-bit data, 256-bit KEK",
+ "input": "00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f",
+ "expectedOutput": "28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21",
+ "recipeConfig": [
+ {
+ "op": "AES Key Wrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Unwrap: RFC Test Vector, 128-bit data, 128-bit KEK",
+ "input": "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5",
+ "expectedOutput": "00112233445566778899aabbccddeeff",
+ "recipeConfig": [
+ {
+ "op": "AES Key Unwrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Unwrap: RFC Test Vector, 128-bit data, 192-bit KEK",
+ "input": "96778b25ae6ca435f92b5b97c050aed2468ab8a17ad84e5d",
+ "expectedOutput": "00112233445566778899aabbccddeeff",
+ "recipeConfig": [
+ {
+ "op": "AES Key Unwrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f1011121314151617"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Unwrap: RFC Test Vector, 128-bit data, 256-bit KEK",
+ "input": "64e8c3f9ce0f5ba263e9777905818a2a93c8191e7d6e8ae7",
+ "expectedOutput": "00112233445566778899aabbccddeeff",
+ "recipeConfig": [
+ {
+ "op": "AES Key Unwrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Unwrap: RFC Test Vector, 192-bit data, 192-bit KEK",
+ "input": "031d33264e15d33268f24ec260743edce1c6c7ddee725a936ba814915c6762d2",
+ "expectedOutput": "00112233445566778899aabbccddeeff0001020304050607",
+ "recipeConfig": [
+ {
+ "op": "AES Key Unwrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f1011121314151617"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Unwrap: RFC Test Vector, 192-bit data, 256-bit KEK",
+ "input": "a8f9bc1612c68b3ff6e6f4fbe30e71e4769c8b80a32cb8958cd5d17d6b254da1",
+ "expectedOutput": "00112233445566778899aabbccddeeff0001020304050607",
+ "recipeConfig": [
+ {
+ "op": "AES Key Unwrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Unwrap: RFC Test Vector, 256-bit data, 256-bit KEK",
+ "input": "28c9f404c4b810f4cbccb35cfb87f8263f5786e2d80ed326cbc7f0e71a99f43bfb988b9b7a02dd21",
+ "expectedOutput": "00112233445566778899aabbccddeeff000102030405060708090a0b0c0d0e0f",
+ "recipeConfig": [
+ {
+ "op": "AES Key Unwrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Wrap: invalid KEK length",
+ "input": "00112233445566778899aabbccddeeff",
+ "expectedOutput": "KEK must be either 16, 24, or 32 bytes (currently 10 bytes)",
+ "recipeConfig": [
+ {
+ "op": "AES Key Wrap",
+ "args": [
+ {"option": "Hex", "string": "00010203040506070809"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Wrap: invalid IV length",
+ "input": "00112233445566778899aabbccddeeff",
+ "expectedOutput": "IV must be 8 bytes (currently 6 bytes)",
+ "recipeConfig": [
+ {
+ "op": "AES Key Wrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Wrap: input length not multiple of 8",
+ "input": "00112233445566778899aabbccddeeff0102",
+ "expectedOutput": "input must be 8n (n>=2) bytes (currently 18 bytes)",
+ "recipeConfig": [
+ {
+ "op": "AES Key Wrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Wrap: input too short",
+ "input": "0011223344556677",
+ "expectedOutput": "input must be 8n (n>=2) bytes (currently 8 bytes)",
+ "recipeConfig": [
+ {
+ "op": "AES Key Wrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Unwrap: invalid KEK length",
+ "input": "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5",
+ "expectedOutput": "KEK must be either 16, 24, or 32 bytes (currently 10 bytes)",
+ "recipeConfig": [
+ {
+ "op": "AES Key Unwrap",
+ "args": [
+ {"option": "Hex", "string": "00010203040506070809"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Unwrap: invalid IV length",
+ "input": "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5",
+ "expectedOutput": "IV must be 8 bytes (currently 6 bytes)",
+ "recipeConfig": [
+ {
+ "op": "AES Key Unwrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Unwrap: input length not multiple of 8",
+ "input": "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5e621",
+ "expectedOutput": "input must be 8n (n>=3) bytes (currently 26 bytes)",
+ "recipeConfig": [
+ {
+ "op": "AES Key Unwrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Unwrap: input too short",
+ "input": "1fa68b0a8112b447aef34bd8fb5a7b82",
+ "expectedOutput": "input must be 8n (n>=3) bytes (currently 16 bytes)",
+ "recipeConfig": [
+ {
+ "op": "AES Key Unwrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+ {
+ "name": "AES Key Unwrap: corrupted input",
+ "input": "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe6",
+ "expectedOutput": "IV mismatch",
+ "recipeConfig": [
+ {
+ "op": "AES Key Unwrap",
+ "args": [
+ {"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
+ {"option": "Hex", "string": "a6a6a6a6a6a6a6a6"},
+ "Hex", "Hex"
+ ],
+ },
+ ],
+ },
+]);