This commit is contained in:
Aaron Osterhage 2025-05-26 21:11:34 +00:00 committed by GitHub
commit 7a027e776e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 449 additions and 80 deletions

View file

@ -155,6 +155,8 @@
"Citrix CTX1 Decode",
"AES Key Wrap",
"AES Key Unwrap",
"AES Key Wrap With Padding",
"AES Key Unwrap With Padding",
"Pseudo-Random Number Generator",
"Enigma",
"Bombe",

View file

@ -0,0 +1,99 @@
/**
* AES Key Wrap/Unwrap defined in RFC 3394
*
* @author aosterhage [aaron.osterhage@gmail.com]
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
import Utils from "../Utils.mjs";
import forge from "node-forge";
/**
* AES Key Wrap algorithm defined in RFC 3394.
*
* @param {string} plaintext
* @param {string} kek
* @param {string} iv
* @returns {string} ciphertext
*/
export function aesKeyWrap(plaintext, kek, iv) {
const cipher = forge.cipher.createCipher("AES-ECB", kek);
let A = iv;
const R = [];
for (let i = 0; i < plaintext.length; i += 8) {
R.push(plaintext.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;
}
}
}
return A + R.join("");
}
/**
* AES Key Unwrap algorithm defined in RFC 3394.
*
* @param {string} ciphertext
* @param {string} kek
* @returns {[string, string]} [plaintext, iv]
*/
export function aesKeyUnwrap(ciphertext, kek) {
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 = ciphertext.substring(0, 8);
const R = [];
for (let i = 8; i < ciphertext.length; i += 8) {
R.push(ciphertext.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;
}
}
}
return [R.join(""), A];
}

View file

@ -5,10 +5,10 @@
*/
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";
import Utils from "../Utils.mjs";
import { aesKeyUnwrap } from "../lib/AESKeyWrap.mjs";
import { toHexFast } from "../lib/Hex.mjs";
/**
* AES Key Unwrap operation
@ -75,52 +75,13 @@ class AESKeyUnwrap extends Operation {
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 [output, outputIv] = aesKeyUnwrap(inputData, kek);
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) {
if (outputIv !== iv) {
throw new OperationError("IV mismatch");
}
const P = R.join("");
if (outputType === "Hex") {
return toHexFast(Utils.strToArrayBuffer(P));
}
return P;
return outputType === "Hex" ? toHexFast(Utils.strToArrayBuffer(output)) : output;
}
}

View file

@ -0,0 +1,98 @@
/**
* @author aosterhage [aaron.osterhage@gmail.com]
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import forge from "node-forge";
import { aesKeyUnwrap } from "../lib/AESKeyWrap.mjs";
import { toHexFast } from "../lib/Hex.mjs";
/**
* AES Key Unwrap With Padding operation
*/
class AESKeyUnwrapWithPadding extends Operation {
/**
* AESKeyUnwrapWithPadding constructor
*/
constructor() {
super();
this.name = "AES Key Unwrap With Padding";
this.module = "Ciphers";
this.description = "Decryptor for a key wrapping algorithm defined in RFC 3394 combined with a padding convention defined in RFC 5649.";
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": "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),
inputType = args[1],
outputType = args[2];
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)");
}
input = Utils.convertToByteString(input, inputType);
if (input.length % 8 !== 0 || input.length < 16) {
throw new OperationError("input must be 8n (n>=2) bytes (currently " + input.length + " bytes)");
}
const decipher = forge.cipher.createDecipher("AES-ECB", kek);
let output, aiv;
if (input.length === 16) {
// Special case where the unwrapped data is one 64-bit block.
decipher.start();
decipher.update(forge.util.createBuffer(input));
decipher.finish();
output = decipher.output.getBytes();
aiv = output.substring(0, 8);
output = output.substring(8, 16);
} else {
// Otherwise, follow the unwrapping process from RFC 3394 (AESKeyUnwrap operation).
[output, aiv] = aesKeyUnwrap(input, kek);
}
// Get the unpadded length from the AIV (which is the MLI). Remove the padding from the output.
const unpaddedLength = Utils.byteArrayToInt(Utils.strToByteArray(aiv.substring(4, 8)), "big");
if (aiv.substring(0, 4) !== "\xa6\x59\x59\xa6" || unpaddedLength > output.length) {
throw new OperationError("invalid AIV found");
}
output = output.substring(0, unpaddedLength);
return outputType === "Hex" ? toHexFast(Utils.strToArrayBuffer(output)) : output;
}
}
export default AESKeyUnwrapWithPadding;

View file

@ -5,10 +5,10 @@
*/
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";
import Utils from "../Utils.mjs";
import { aesKeyWrap } from "../lib/AESKeyWrap.mjs";
import { toHexFast } from "../lib/Hex.mjs";
/**
* AES Key Wrap operation
@ -70,44 +70,15 @@ class AESKeyWrap extends Operation {
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);
const output = aesKeyWrap(inputData, kek, iv);
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;
return outputType === "Hex" ? toHexFast(Utils.strToArrayBuffer(output)) : output;
}
}

View file

@ -0,0 +1,101 @@
/**
* @author aosterhage [aaron.osterhage@gmail.com]
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import Utils from "../Utils.mjs";
import forge from "node-forge";
import { aesKeyWrap } from "../lib/AESKeyWrap.mjs";
import { toHexFast } from "../lib/Hex.mjs";
/**
* AES Key Wrap With Padding operation
*/
class AESKeyWrapWithPadding extends Operation {
/**
* AESKeyWrapWithPadding constructor
*/
constructor() {
super();
this.name = "AES Key Wrap With Padding";
this.module = "Ciphers";
this.description = "A key wrapping algorithm defined in RFC 3394 combined with a padding convention defined in RFC 5649.<br><br>The padding convention defined in RFC 5649 eliminates the requirement that the length of the key to be wrapped be a multiple of 64 bits, allowing a key of any practical length to be wrapped.";
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": "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),
inputType = args[1],
outputType = args[2];
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)");
}
input = Utils.convertToByteString(input, inputType);
if (input.length <= 0) {
throw new OperationError("input must be > 0 bytes");
}
// Construct the "Alternative Initial Value" (AIV).
const aiv = "\xa6\x59\x59\xa6" + Utils.byteArrayToChars(Utils.intToByteArray(input.length, 4, "big"));;
// Pad the input as needed.
const isMultipleOf8 = (input.length % 8) === 0;
const paddedLength = input.length + (isMultipleOf8 ? 0 : (8 - (input.length % 8)));
input = input.padEnd(paddedLength, "\0");
let output;
if (paddedLength === 8) {
// Special case where the padded input is one 64-bit block.
// Get the cipher ready and disable PKCS#7 padding.
const cipher = forge.cipher.createCipher("AES-ECB", kek);
cipher.mode.pad = false;
cipher.start();
cipher.update(forge.util.createBuffer(aiv + input));
cipher.finish();
output = cipher.output.getBytes();
} else {
// Otherwise, follow the wrapping process from RFC 3394 (AESKeyWrap operation).
output = aesKeyWrap(input, kek, aiv);
}
return outputType === "Hex" ? toHexFast(Utils.strToArrayBuffer(output)) : output;
}
}
export default AESKeyWrapWithPadding;

View file

@ -15,6 +15,7 @@ import { setLongTestFailure, logTestReport } from "../lib/utils.mjs";
import TestRegister from "../lib/TestRegister.mjs";
import "./tests/AESKeyWrap.mjs";
import "./tests/AESKeyWrapWithPadding.mjs";
import "./tests/AlternatingCaps.mjs";
import "./tests/AvroToJSON.mjs";
import "./tests/BaconCipher.mjs";

View file

@ -0,0 +1,136 @@
/**
* @author aosterhage [aaron.osterhage@gmail.com]
* @copyright Crown Copyright 2025
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
"name": "AES Key Wrap With Padding: RFC Test Vector, 20 octets data, 192-bit KEK",
"input": "c37b7e6492584340bed12207808941155068f738",
"expectedOutput": "138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a",
"recipeConfig": [
{
"op": "AES Key Wrap With Padding",
"args": [
{"option": "Hex", "string": "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"},
"Hex", "Hex"
],
},
],
},
{
"name": "AES Key Wrap With Padding: RFC Test Vector, 7 octets data, 192-bit KEK",
"input": "466f7250617369",
"expectedOutput": "afbeb0f07dfbf5419200f2ccb50bb24f",
"recipeConfig": [
{
"op": "AES Key Wrap With Padding",
"args": [
{"option": "Hex", "string": "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"},
"Hex", "Hex"
],
},
],
},
{
"name": "AES Key Wrap With Padding: invalid KEK length",
"input": "00112233445566778899aabbccddeeff",
"expectedOutput": "KEK must be either 16, 24, or 32 bytes (currently 10 bytes)",
"recipeConfig": [
{
"op": "AES Key Wrap With Padding",
"args": [
{"option": "Hex", "string": "00010203040506070809"},
"Hex", "Hex"
],
},
],
},
{
"name": "AES Key Wrap With Padding: input too short",
"input": "",
"expectedOutput": "input must be > 0 bytes",
"recipeConfig": [
{
"op": "AES Key Wrap With Padding",
"args": [
{"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
"Hex", "Hex"
],
},
],
},
{
"name": "AES Key Unwrap With Padding: RFC Test Vector, 20 octets data, 192-bit KEK",
"input": "138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a",
"expectedOutput": "c37b7e6492584340bed12207808941155068f738",
"recipeConfig": [
{
"op": "AES Key Unwrap With Padding",
"args": [
{"option": "Hex", "string": "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"},
"Hex", "Hex"
],
},
],
},
{
"name": "AES Key Unwrap With Padding: RFC Test Vector, 7 octets data, 192-bit KEK",
"input": "afbeb0f07dfbf5419200f2ccb50bb24f",
"expectedOutput": "466f7250617369",
"recipeConfig": [
{
"op": "AES Key Unwrap With Padding",
"args": [
{"option": "Hex", "string": "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8"},
"Hex", "Hex"
],
},
],
},
{
"name": "AES Key Unwrap With Padding: invalid KEK length",
"input": "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5",
"expectedOutput": "KEK must be either 16, 24, or 32 bytes (currently 10 bytes)",
"recipeConfig": [
{
"op": "AES Key Unwrap With Padding",
"args": [
{"option": "Hex", "string": "00010203040506070809"},
"Hex", "Hex"
],
},
],
},
{
"name": "AES Key Unwrap With Padding: input length not multiple of 8",
"input": "1fa68b0a8112b447aef34bd8fb5a7b829d3e862371d2cfe5e621",
"expectedOutput": "input must be 8n (n>=2) bytes (currently 26 bytes)",
"recipeConfig": [
{
"op": "AES Key Unwrap With Padding",
"args": [
{"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
"Hex", "Hex"
],
},
],
},
{
"name": "AES Key Unwrap With Padding: input too short",
"input": "1fa68b0a8112b447",
"expectedOutput": "input must be 8n (n>=2) bytes (currently 8 bytes)",
"recipeConfig": [
{
"op": "AES Key Unwrap With Padding",
"args": [
{"option": "Hex", "string": "000102030405060708090a0b0c0d0e0f"},
"Hex", "Hex"
],
},
],
},
]);