Skip to content

Commit dbcbafc

Browse files
committed
add new operations: AES Key Wrap/Unwrap With Padding
1 parent c57556f commit dbcbafc

File tree

5 files changed

+398
-0
lines changed

5 files changed

+398
-0
lines changed

src/core/config/Categories.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@
155155
"Citrix CTX1 Decode",
156156
"AES Key Wrap",
157157
"AES Key Unwrap",
158+
"AES Key Wrap With Padding",
159+
"AES Key Unwrap With Padding",
158160
"Pseudo-Random Number Generator",
159161
"Enigma",
160162
"Bombe",
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/**
2+
* @author aosterhage [aaron.osterhage@gmail.com]
3+
* @copyright Crown Copyright 2025
4+
* @license Apache-2.0
5+
*/
6+
7+
import Operation from "../Operation.mjs";
8+
import OperationError from "../errors/OperationError.mjs";
9+
import Utils from "../Utils.mjs";
10+
import { toHexFast } from "../lib/Hex.mjs";
11+
import forge from "node-forge";
12+
13+
/**
14+
* AES Key Unwrap With Padding operation
15+
*/
16+
class AESKeyUnwrapWithPadding extends Operation {
17+
18+
/**
19+
* AESKeyUnwrapWithPadding constructor
20+
*/
21+
constructor() {
22+
super();
23+
24+
this.name = "AES Key Unwrap With Padding";
25+
this.module = "Ciphers";
26+
this.description = "Decryptor for a key wrapping algorithm defined in RFC 3394 combined with a padding convention defined in RFC 5649.";
27+
this.infoURL = "https://wikipedia.org/wiki/Key_wrap";
28+
this.inputType = "string";
29+
this.outputType = "string";
30+
this.args = [
31+
{
32+
"name": "Key (KEK)",
33+
"type": "toggleString",
34+
"value": "",
35+
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
36+
},
37+
{
38+
"name": "Input",
39+
"type": "option",
40+
"value": ["Hex", "Raw"]
41+
},
42+
{
43+
"name": "Output",
44+
"type": "option",
45+
"value": ["Hex", "Raw"]
46+
},
47+
];
48+
}
49+
50+
/**
51+
* @param {string} input
52+
* @param {Object[]} args
53+
* @returns {string}
54+
*/
55+
run(input, args) {
56+
const kek = Utils.convertToByteString(args[0].string, args[0].option),
57+
inputType = args[1],
58+
outputType = args[2];
59+
60+
if (kek.length !== 16 && kek.length !== 24 && kek.length !== 32) {
61+
throw new OperationError("KEK must be either 16, 24, or 32 bytes (currently " + kek.length + " bytes)");
62+
}
63+
64+
input = Utils.convertToByteString(input, inputType);
65+
if (input.length % 8 !== 0 || input.length < 16) {
66+
throw new OperationError("input must be 8n (n>=2) bytes (currently " + input.length + " bytes)");
67+
}
68+
69+
const decipher = forge.cipher.createDecipher("AES-ECB", kek);
70+
let output, aiv;
71+
72+
if (input.length === 16) {
73+
// Special case where the unwrapped data is one 64-bit block.
74+
decipher.start();
75+
decipher.update(forge.util.createBuffer(input));
76+
decipher.finish();
77+
output = decipher.output.getBytes();
78+
aiv = output.substring(0, 8);
79+
output = output.substring(8, 16);
80+
} else {
81+
// Otherwise, follow the unwrapping process from RFC 3394 (AESKeyUnwrap operation).
82+
const cipher = forge.cipher.createCipher("AES-ECB", kek);
83+
cipher.start();
84+
cipher.update(forge.util.createBuffer(""));
85+
cipher.finish();
86+
const paddingBlock = cipher.output.getBytes();
87+
88+
let A = input.substring(0, 8);
89+
const R = [];
90+
for (let i = 8; i < input.length; i += 8) {
91+
R.push(input.substring(i, i + 8));
92+
}
93+
let cntLower = R.length >>> 0;
94+
let cntUpper = (R.length / ((1 << 30) * 4)) >>> 0;
95+
cntUpper = cntUpper * 6 + ((cntLower * 6 / ((1 << 30) * 4)) >>> 0);
96+
cntLower = cntLower * 6 >>> 0;
97+
for (let j = 5; j >= 0; j--) {
98+
for (let i = R.length - 1; i >= 0; i--) {
99+
const aBuffer = Utils.strToArrayBuffer(A);
100+
const aView = new DataView(aBuffer);
101+
aView.setUint32(0, aView.getUint32(0) ^ cntUpper);
102+
aView.setUint32(4, aView.getUint32(4) ^ cntLower);
103+
A = Utils.arrayBufferToStr(aBuffer, false);
104+
decipher.start();
105+
decipher.update(forge.util.createBuffer(A + R[i] + paddingBlock));
106+
decipher.finish();
107+
const B = decipher.output.getBytes();
108+
A = B.substring(0, 8);
109+
R[i] = B.substring(8, 16);
110+
cntLower--;
111+
if (cntLower < 0) {
112+
cntUpper--;
113+
cntLower = 0xffffffff;
114+
}
115+
}
116+
}
117+
output = R.join("");
118+
aiv = A;
119+
}
120+
121+
// Get the unpadded length from the AIV (which is the MLI). Remove the padding from the output.
122+
const unpaddedLength = Utils.byteArrayToInt(Utils.strToByteArray(aiv.substring(4, 8)), "big");
123+
output = output.substring(0, unpaddedLength);
124+
125+
if (outputType == "Hex") {
126+
output = toHexFast(Utils.strToArrayBuffer(output));
127+
}
128+
return output;
129+
}
130+
131+
}
132+
133+
export default AESKeyUnwrapWithPadding;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* @author aosterhage [aaron.osterhage@gmail.com]
3+
* @copyright Crown Copyright 2025
4+
* @license Apache-2.0
5+
*/
6+
7+
import Operation from "../Operation.mjs";
8+
import OperationError from "../errors/OperationError.mjs";
9+
import Utils from "../Utils.mjs";
10+
import { toHexFast } from "../lib/Hex.mjs";
11+
import forge from "node-forge";
12+
13+
/**
14+
* AES Key Wrap With Padding operation
15+
*/
16+
class AESKeyWrapWithPadding extends Operation {
17+
18+
/**
19+
* AESKeyWrapWithPadding constructor
20+
*/
21+
constructor() {
22+
super();
23+
24+
this.name = "AES Key Wrap With Padding";
25+
this.module = "Ciphers";
26+
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.";
27+
this.infoURL = "https://wikipedia.org/wiki/Key_wrap";
28+
this.inputType = "string";
29+
this.outputType = "string";
30+
this.args = [
31+
{
32+
"name": "Key (KEK)",
33+
"type": "toggleString",
34+
"value": "",
35+
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
36+
},
37+
{
38+
"name": "Input",
39+
"type": "option",
40+
"value": ["Hex", "Raw"]
41+
},
42+
{
43+
"name": "Output",
44+
"type": "option",
45+
"value": ["Hex", "Raw"]
46+
},
47+
];
48+
}
49+
50+
/**
51+
* @param {string} input
52+
* @param {Object[]} args
53+
* @returns {string}
54+
*/
55+
run(input, args) {
56+
const kek = Utils.convertToByteString(args[0].string, args[0].option),
57+
inputType = args[1],
58+
outputType = args[2];
59+
60+
if (kek.length !== 16 && kek.length !== 24 && kek.length !== 32) {
61+
throw new OperationError("KEK must be either 16, 24, or 32 bytes (currently " + kek.length + " bytes)");
62+
}
63+
64+
input = Utils.convertToByteString(input, inputType);
65+
if (input.length <= 0) {
66+
throw new OperationError("input must be > 0 bytes");
67+
}
68+
69+
// Construct the "Alternative Initial Value" (AIV).
70+
const aiv = "\xa6\x59\x59\xa6" + Utils.byteArrayToChars(Utils.intToByteArray(input.length, 4, "big"));;
71+
72+
// Pad the input as needed.
73+
const isMultipleOf8 = (input.length % 8) === 0;
74+
const paddedLength = input.length + (isMultipleOf8 ? 0 : (8 - (input.length % 8)));
75+
input = input.padEnd(paddedLength, "\0");
76+
77+
// Get the cipher ready and disable PKCS#7 padding.
78+
const cipher = forge.cipher.createCipher("AES-ECB", kek);
79+
cipher.mode.pad = false;
80+
let output;
81+
82+
if (paddedLength === 8) {
83+
// Special case where the padded input is one 64-bit block.
84+
cipher.start();
85+
cipher.update(forge.util.createBuffer(aiv + input));
86+
cipher.finish();
87+
output = cipher.output.getBytes();
88+
} else {
89+
// Otherwise, follow the wrapping process from RFC 3394 (AESKeyWrap operation).
90+
let A = aiv;
91+
const R = [];
92+
for (let i = 0; i < input.length; i += 8) {
93+
R.push(input.substring(i, i + 8));
94+
}
95+
let cntLower = 1, cntUpper = 0;
96+
for (let j = 0; j < 6; j++) {
97+
for (let i = 0; i < R.length; i++) {
98+
cipher.start();
99+
cipher.update(forge.util.createBuffer(A + R[i]));
100+
cipher.finish();
101+
const B = cipher.output.getBytes();
102+
const msbBuffer = Utils.strToArrayBuffer(B.substring(0, 8));
103+
const msbView = new DataView(msbBuffer);
104+
msbView.setUint32(0, msbView.getUint32(0) ^ cntUpper);
105+
msbView.setUint32(4, msbView.getUint32(4) ^ cntLower);
106+
A = Utils.arrayBufferToStr(msbBuffer, false);
107+
R[i] = B.substring(8, 16);
108+
cntLower++;
109+
if (cntLower > 0xffffffff) {
110+
cntUpper++;
111+
cntLower = 0;
112+
}
113+
}
114+
}
115+
output = A + R.join("");
116+
}
117+
118+
if (outputType == "Hex") {
119+
output = toHexFast(Utils.strToArrayBuffer(output));
120+
}
121+
return output;
122+
}
123+
124+
}
125+
126+
export default AESKeyWrapWithPadding;

tests/operations/index.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { setLongTestFailure, logTestReport } from "../lib/utils.mjs";
1515

1616
import TestRegister from "../lib/TestRegister.mjs";
1717
import "./tests/AESKeyWrap.mjs";
18+
import "./tests/AESKeyWrapWithPadding.mjs";
1819
import "./tests/AlternatingCaps.mjs";
1920
import "./tests/AvroToJSON.mjs";
2021
import "./tests/BaconCipher.mjs";

0 commit comments

Comments
 (0)