add new operations: AES Key Wrap/Unwrap With Padding

This commit is contained in:
Aaron Osterhage 2025-05-26 00:53:17 -04:00
parent c57556f49f
commit 25dcab0a52
8 changed files with 449 additions and 80 deletions

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;