Skip to content

Commit 25dcab0

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

File tree

8 files changed

+447
-78
lines changed

8 files changed

+447
-78
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",

src/core/lib/AESKeyWrap.mjs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* AES Key Wrap/Unwrap defined in RFC 3394
3+
*
4+
* @author aosterhage [[email protected]]
5+
* @copyright Crown Copyright 2025
6+
* @license Apache-2.0
7+
*/
8+
9+
import Utils from "../Utils.mjs";
10+
import forge from "node-forge";
11+
12+
/**
13+
* AES Key Wrap algorithm defined in RFC 3394.
14+
*
15+
* @param {string} plaintext
16+
* @param {string} kek
17+
* @param {string} iv
18+
* @returns {string} ciphertext
19+
*/
20+
export function aesKeyWrap(plaintext, kek, iv) {
21+
const cipher = forge.cipher.createCipher("AES-ECB", kek);
22+
23+
let A = iv;
24+
const R = [];
25+
for (let i = 0; i < plaintext.length; i += 8) {
26+
R.push(plaintext.substring(i, i + 8));
27+
}
28+
let cntLower = 1, cntUpper = 0;
29+
for (let j = 0; j < 6; j++) {
30+
for (let i = 0; i < R.length; i++) {
31+
cipher.start();
32+
cipher.update(forge.util.createBuffer(A + R[i]));
33+
cipher.finish();
34+
const B = cipher.output.getBytes();
35+
const msbBuffer = Utils.strToArrayBuffer(B.substring(0, 8));
36+
const msbView = new DataView(msbBuffer);
37+
msbView.setUint32(0, msbView.getUint32(0) ^ cntUpper);
38+
msbView.setUint32(4, msbView.getUint32(4) ^ cntLower);
39+
A = Utils.arrayBufferToStr(msbBuffer, false);
40+
R[i] = B.substring(8, 16);
41+
cntLower++;
42+
if (cntLower > 0xffffffff) {
43+
cntUpper++;
44+
cntLower = 0;
45+
}
46+
}
47+
}
48+
49+
return A + R.join("");
50+
}
51+
52+
/**
53+
* AES Key Unwrap algorithm defined in RFC 3394.
54+
*
55+
* @param {string} ciphertext
56+
* @param {string} kek
57+
* @returns {[string, string]} [plaintext, iv]
58+
*/
59+
export function aesKeyUnwrap(ciphertext, kek) {
60+
const cipher = forge.cipher.createCipher("AES-ECB", kek);
61+
cipher.start();
62+
cipher.update(forge.util.createBuffer(""));
63+
cipher.finish();
64+
const paddingBlock = cipher.output.getBytes();
65+
66+
const decipher = forge.cipher.createDecipher("AES-ECB", kek);
67+
68+
let A = ciphertext.substring(0, 8);
69+
const R = [];
70+
for (let i = 8; i < ciphertext.length; i += 8) {
71+
R.push(ciphertext.substring(i, i + 8));
72+
}
73+
let cntLower = R.length >>> 0;
74+
let cntUpper = (R.length / ((1 << 30) * 4)) >>> 0;
75+
cntUpper = cntUpper * 6 + ((cntLower * 6 / ((1 << 30) * 4)) >>> 0);
76+
cntLower = cntLower * 6 >>> 0;
77+
for (let j = 5; j >= 0; j--) {
78+
for (let i = R.length - 1; i >= 0; i--) {
79+
const aBuffer = Utils.strToArrayBuffer(A);
80+
const aView = new DataView(aBuffer);
81+
aView.setUint32(0, aView.getUint32(0) ^ cntUpper);
82+
aView.setUint32(4, aView.getUint32(4) ^ cntLower);
83+
A = Utils.arrayBufferToStr(aBuffer, false);
84+
decipher.start();
85+
decipher.update(forge.util.createBuffer(A + R[i] + paddingBlock));
86+
decipher.finish();
87+
const B = decipher.output.getBytes();
88+
A = B.substring(0, 8);
89+
R[i] = B.substring(8, 16);
90+
cntLower--;
91+
if (cntLower < 0) {
92+
cntUpper--;
93+
cntLower = 0xffffffff;
94+
}
95+
}
96+
}
97+
98+
return [R.join(""), A];
99+
}

src/core/operations/AESKeyUnwrap.mjs

Lines changed: 5 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
*/
66

77
import Operation from "../Operation.mjs";
8+
import OperationError from "../errors/OperationError.mjs";
89
import Utils from "../Utils.mjs";
10+
import { aesKeyUnwrap } from "../lib/AESKeyWrap.mjs";
911
import { toHexFast } from "../lib/Hex.mjs";
10-
import forge from "node-forge";
11-
import OperationError from "../errors/OperationError.mjs";
1212

1313
/**
1414
* AES Key Unwrap operation
@@ -75,52 +75,13 @@ class AESKeyUnwrap extends Operation {
7575
throw new OperationError("input must be 8n (n>=3) bytes (currently " + inputData.length + " bytes)");
7676
}
7777

78-
const cipher = forge.cipher.createCipher("AES-ECB", kek);
79-
cipher.start();
80-
cipher.update(forge.util.createBuffer(""));
81-
cipher.finish();
82-
const paddingBlock = cipher.output.getBytes();
83-
84-
const decipher = forge.cipher.createDecipher("AES-ECB", kek);
78+
const [output, outputIv] = aesKeyUnwrap(inputData, kek);
8579

86-
let A = inputData.substring(0, 8);
87-
const R = [];
88-
for (let i = 8; i < inputData.length; i += 8) {
89-
R.push(inputData.substring(i, i + 8));
90-
}
91-
let cntLower = R.length >>> 0;
92-
let cntUpper = (R.length / ((1 << 30) * 4)) >>> 0;
93-
cntUpper = cntUpper * 6 + ((cntLower * 6 / ((1 << 30) * 4)) >>> 0);
94-
cntLower = cntLower * 6 >>> 0;
95-
for (let j = 5; j >= 0; j--) {
96-
for (let i = R.length - 1; i >= 0; i--) {
97-
const aBuffer = Utils.strToArrayBuffer(A);
98-
const aView = new DataView(aBuffer);
99-
aView.setUint32(0, aView.getUint32(0) ^ cntUpper);
100-
aView.setUint32(4, aView.getUint32(4) ^ cntLower);
101-
A = Utils.arrayBufferToStr(aBuffer, false);
102-
decipher.start();
103-
decipher.update(forge.util.createBuffer(A + R[i] + paddingBlock));
104-
decipher.finish();
105-
const B = decipher.output.getBytes();
106-
A = B.substring(0, 8);
107-
R[i] = B.substring(8, 16);
108-
cntLower--;
109-
if (cntLower < 0) {
110-
cntUpper--;
111-
cntLower = 0xffffffff;
112-
}
113-
}
114-
}
115-
if (A !== iv) {
80+
if (outputIv !== iv) {
11681
throw new OperationError("IV mismatch");
11782
}
118-
const P = R.join("");
11983

120-
if (outputType === "Hex") {
121-
return toHexFast(Utils.strToArrayBuffer(P));
122-
}
123-
return P;
84+
return outputType === "Hex" ? toHexFast(Utils.strToArrayBuffer(output)) : output;
12485
}
12586

12687
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/**
2+
* @author aosterhage [[email protected]]
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 forge from "node-forge";
11+
import { aesKeyUnwrap } from "../lib/AESKeyWrap.mjs";
12+
import { toHexFast } from "../lib/Hex.mjs";
13+
14+
/**
15+
* AES Key Unwrap With Padding operation
16+
*/
17+
class AESKeyUnwrapWithPadding extends Operation {
18+
19+
/**
20+
* AESKeyUnwrapWithPadding constructor
21+
*/
22+
constructor() {
23+
super();
24+
25+
this.name = "AES Key Unwrap With Padding";
26+
this.module = "Ciphers";
27+
this.description = "Decryptor for a key wrapping algorithm defined in RFC 3394 combined with a padding convention defined in RFC 5649.";
28+
this.infoURL = "https://wikipedia.org/wiki/Key_wrap";
29+
this.inputType = "string";
30+
this.outputType = "string";
31+
this.args = [
32+
{
33+
"name": "Key (KEK)",
34+
"type": "toggleString",
35+
"value": "",
36+
"toggleValues": ["Hex", "UTF8", "Latin1", "Base64"]
37+
},
38+
{
39+
"name": "Input",
40+
"type": "option",
41+
"value": ["Hex", "Raw"]
42+
},
43+
{
44+
"name": "Output",
45+
"type": "option",
46+
"value": ["Hex", "Raw"]
47+
},
48+
];
49+
}
50+
51+
/**
52+
* @param {string} input
53+
* @param {Object[]} args
54+
* @returns {string}
55+
*/
56+
run(input, args) {
57+
const kek = Utils.convertToByteString(args[0].string, args[0].option),
58+
inputType = args[1],
59+
outputType = args[2];
60+
61+
if (kek.length !== 16 && kek.length !== 24 && kek.length !== 32) {
62+
throw new OperationError("KEK must be either 16, 24, or 32 bytes (currently " + kek.length + " bytes)");
63+
}
64+
65+
input = Utils.convertToByteString(input, inputType);
66+
if (input.length % 8 !== 0 || input.length < 16) {
67+
throw new OperationError("input must be 8n (n>=2) bytes (currently " + input.length + " bytes)");
68+
}
69+
70+
const decipher = forge.cipher.createDecipher("AES-ECB", kek);
71+
let output, aiv;
72+
73+
if (input.length === 16) {
74+
// Special case where the unwrapped data is one 64-bit block.
75+
decipher.start();
76+
decipher.update(forge.util.createBuffer(input));
77+
decipher.finish();
78+
output = decipher.output.getBytes();
79+
aiv = output.substring(0, 8);
80+
output = output.substring(8, 16);
81+
} else {
82+
// Otherwise, follow the unwrapping process from RFC 3394 (AESKeyUnwrap operation).
83+
[output, aiv] = aesKeyUnwrap(input, kek);
84+
}
85+
86+
// Get the unpadded length from the AIV (which is the MLI). Remove the padding from the output.
87+
const unpaddedLength = Utils.byteArrayToInt(Utils.strToByteArray(aiv.substring(4, 8)), "big");
88+
if (aiv.substring(0, 4) !== "\xa6\x59\x59\xa6" || unpaddedLength > output.length) {
89+
throw new OperationError("invalid AIV found");
90+
}
91+
output = output.substring(0, unpaddedLength);
92+
93+
return outputType === "Hex" ? toHexFast(Utils.strToArrayBuffer(output)) : output;
94+
}
95+
96+
}
97+
98+
export default AESKeyUnwrapWithPadding;

src/core/operations/AESKeyWrap.mjs

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
*/
66

77
import Operation from "../Operation.mjs";
8+
import OperationError from "../errors/OperationError.mjs";
89
import Utils from "../Utils.mjs";
10+
import { aesKeyWrap } from "../lib/AESKeyWrap.mjs";
911
import { toHexFast } from "../lib/Hex.mjs";
10-
import forge from "node-forge";
11-
import OperationError from "../errors/OperationError.mjs";
1212

1313
/**
1414
* AES Key Wrap operation
@@ -70,44 +70,15 @@ class AESKeyWrap extends Operation {
7070
if (iv.length !== 8) {
7171
throw new OperationError("IV must be 8 bytes (currently " + iv.length + " bytes)");
7272
}
73+
7374
const inputData = Utils.convertToByteString(input, inputType);
7475
if (inputData.length % 8 !== 0 || inputData.length < 16) {
7576
throw new OperationError("input must be 8n (n>=2) bytes (currently " + inputData.length + " bytes)");
7677
}
7778

78-
const cipher = forge.cipher.createCipher("AES-ECB", kek);
79+
const output = aesKeyWrap(inputData, kek, iv);
7980

80-
let A = iv;
81-
const R = [];
82-
for (let i = 0; i < inputData.length; i += 8) {
83-
R.push(inputData.substring(i, i + 8));
84-
}
85-
let cntLower = 1, cntUpper = 0;
86-
for (let j = 0; j < 6; j++) {
87-
for (let i = 0; i < R.length; i++) {
88-
cipher.start();
89-
cipher.update(forge.util.createBuffer(A + R[i]));
90-
cipher.finish();
91-
const B = cipher.output.getBytes();
92-
const msbBuffer = Utils.strToArrayBuffer(B.substring(0, 8));
93-
const msbView = new DataView(msbBuffer);
94-
msbView.setUint32(0, msbView.getUint32(0) ^ cntUpper);
95-
msbView.setUint32(4, msbView.getUint32(4) ^ cntLower);
96-
A = Utils.arrayBufferToStr(msbBuffer, false);
97-
R[i] = B.substring(8, 16);
98-
cntLower++;
99-
if (cntLower > 0xffffffff) {
100-
cntUpper++;
101-
cntLower = 0;
102-
}
103-
}
104-
}
105-
const C = A + R.join("");
106-
107-
if (outputType === "Hex") {
108-
return toHexFast(Utils.strToArrayBuffer(C));
109-
}
110-
return C;
81+
return outputType === "Hex" ? toHexFast(Utils.strToArrayBuffer(output)) : output;
11182
}
11283

11384
}

0 commit comments

Comments
 (0)