Add Key Component Split and Combine operations (issue #2)
XOR key ceremony helpers: split a key into 2-8 components and recombine. Chains cleanly with Key Generate and wrap/encrypt operations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
79ac440b14
commit
28cda9bad9
@ -212,6 +212,8 @@ Operations:
|
|||||||
- `DUKPT Derive AES Key` — AES-128 DUKPT per ANSI X9.24-3 (12-byte KSN, IK-based)
|
- `DUKPT Derive AES Key` — AES-128 DUKPT per ANSI X9.24-3 (12-byte KSN, IK-based)
|
||||||
- `Derive ECDH Key Material`
|
- `Derive ECDH Key Material`
|
||||||
- `Key Generate` — random AES-128/192/256, TDES, or custom bytes; optional AES CMAC KCV
|
- `Key Generate` — random AES-128/192/256, TDES, or custom bytes; optional AES CMAC KCV
|
||||||
|
- `Key Component Split` — XOR-split a key into 2–8 components for key ceremony use
|
||||||
|
- `Key Component Combine` — XOR-combine components back into the original key
|
||||||
- `Payment Calculate KCV`
|
- `Payment Calculate KCV`
|
||||||
- `AS2805 Generate KEK Validation`
|
- `AS2805 Generate KEK Validation`
|
||||||
|
|
||||||
@ -219,6 +221,8 @@ Use this when:
|
|||||||
- you need transaction keys, shared secrets, random test keys, KCVs, or AS2805-style KEK-validation lab values
|
- you need transaction keys, shared secrets, random test keys, KCVs, or AS2805-style KEK-validation lab values
|
||||||
|
|
||||||
Important assumptions:
|
Important assumptions:
|
||||||
|
- `Key Component Split` and `Key Component Combine` use XOR shares — all N components are required to reconstruct the key (no threshold/Shamir scheme)
|
||||||
|
- These operations are intended for testing and emulation, not production key ceremonies — production ceremonies must use a certified HSM
|
||||||
- `DUKPT Derive TDES Key` is TDES DUKPT — do not confuse IPEK (TDES) with IK (AES DUKPT)
|
- `DUKPT Derive TDES Key` is TDES DUKPT — do not confuse IPEK (TDES) with IK (AES DUKPT)
|
||||||
- `DUKPT Derive AES Key` implements AES-128 via AES-CMAC per ANSI X9.24-3; AES-192/256 are not yet implemented
|
- `DUKPT Derive AES Key` implements AES-128 via AES-CMAC per ANSI X9.24-3; AES-192/256 are not yet implemented
|
||||||
- `Key Generate` is for test use only — production keys must be generated in an approved HSM
|
- `Key Generate` is for test use only — production keys must be generated in an approved HSM
|
||||||
@ -396,6 +400,8 @@ Release guidance: `Publish` = safe with normal guardrails; `Publish with guardra
|
|||||||
| `PIN Block Translate Encrypted` | Vendor-aligned | AWS `TranslatePinData`; ISO 9564; PCI PIN Req 3-3 | Publish with guardrails |
|
| `PIN Block Translate Encrypted` | Vendor-aligned | AWS `TranslatePinData`; ISO 9564; PCI PIN Req 3-3 | Publish with guardrails |
|
||||||
| `PIN Data Generate` | Vendor-aligned | AWS `GeneratePinData` | Publish with guardrails |
|
| `PIN Data Generate` | Vendor-aligned | AWS `GeneratePinData` | Publish with guardrails |
|
||||||
| `PIN Data Verify` | Vendor-aligned | AWS `VerifyPinData` | Publish with guardrails |
|
| `PIN Data Verify` | Vendor-aligned | AWS `VerifyPinData` | Publish with guardrails |
|
||||||
|
| `Key Component Split` | Verified | XOR key split — standard PCI key ceremony primitive | Publish with guardrails |
|
||||||
|
| `Key Component Combine` | Verified | XOR key combine — standard PCI key ceremony primitive | Publish with guardrails |
|
||||||
| `Payment Calculate KCV` | Verified | NIST SP 800-38B; generic AES/TDES/HMAC primitives | Publish |
|
| `Payment Calculate KCV` | Verified | NIST SP 800-38B; generic AES/TDES/HMAC primitives | Publish |
|
||||||
| `DUKPT Derive TDES Key` | Externally cross-checked | ANSI X9.24-1; AWS DUKPT terminology | Publish with guardrails |
|
| `DUKPT Derive TDES Key` | Externally cross-checked | ANSI X9.24-1; AWS DUKPT terminology | Publish with guardrails |
|
||||||
| `DUKPT Derive AES Key` | Externally cross-checked | ANSI X9.24-3 §6.3 official test vectors (x9.org) | Publish with guardrails |
|
| `DUKPT Derive AES Key` | Externally cross-checked | ANSI X9.24-3 §6.3 official test vectors (x9.org) | Publish with guardrails |
|
||||||
|
|||||||
@ -599,6 +599,8 @@
|
|||||||
"EMV Verify MAC",
|
"EMV Verify MAC",
|
||||||
"HSM Parse Futurex Command",
|
"HSM Parse Futurex Command",
|
||||||
"HSM Parse Thales Command",
|
"HSM Parse Thales Command",
|
||||||
|
"Key Component Combine",
|
||||||
|
"Key Component Split",
|
||||||
"Key Generate",
|
"Key Generate",
|
||||||
"MAC Generate",
|
"MAC Generate",
|
||||||
"MAC Verify",
|
"MAC Verify",
|
||||||
|
|||||||
101
src/core/operations/KeyComponentCombine.mjs
Normal file
101
src/core/operations/KeyComponentCombine.mjs
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* @author Jacob Marks [jacob.marks@jacobmarks.com]
|
||||||
|
* @copyright Jacob Marks 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key Component Combine operation
|
||||||
|
*/
|
||||||
|
class KeyComponentCombine extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyComponentCombine constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Key Component Combine";
|
||||||
|
this.module = "Payment";
|
||||||
|
this.description = "Combines XOR key components into the original key. Each component is XOR'd together to reconstruct the key. Accepts 2–8 components.<br><br>Input: one hex component per line, or JSON output from Key Component Split. Plain hex output chains directly into wrap and encryption operations.";
|
||||||
|
this.infoURL = "";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
name: "Output as JSON",
|
||||||
|
type: "boolean",
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.testDataSamples = [{
|
||||||
|
input: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\nFEDCBA98765432100123456789ABCDEF",
|
||||||
|
args: [false]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
const [outputJson] = args;
|
||||||
|
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!trimmed) throw new Error("Input is empty.");
|
||||||
|
|
||||||
|
let hexComponents;
|
||||||
|
if (trimmed.startsWith("{")) {
|
||||||
|
let parsed;
|
||||||
|
try { parsed = JSON.parse(trimmed); } catch (e) {
|
||||||
|
throw new Error("Invalid JSON input.");
|
||||||
|
}
|
||||||
|
if (!Array.isArray(parsed.components) || parsed.components.length === 0) {
|
||||||
|
throw new Error("JSON input must contain a non-empty 'components' array.");
|
||||||
|
}
|
||||||
|
hexComponents = parsed.components;
|
||||||
|
} else {
|
||||||
|
hexComponents = trimmed.split("\n")
|
||||||
|
.map(l => l.trim().toUpperCase().replace(/\s+/g, ""))
|
||||||
|
.filter(l => l.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hexComponents.length < 2) throw new Error("At least 2 components are required.");
|
||||||
|
if (hexComponents.length > 8) throw new Error("Maximum 8 components are supported.");
|
||||||
|
|
||||||
|
for (const hex of hexComponents) {
|
||||||
|
if (!/^[0-9A-F]+$/.test(hex) || hex.length % 2 !== 0) {
|
||||||
|
throw new Error(`Invalid hex component: ${hex.slice(0, 16)}${hex.length > 16 ? "…" : ""}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const byteLen = hexComponents[0].length / 2;
|
||||||
|
if (hexComponents.some(h => h.length / 2 !== byteLen)) {
|
||||||
|
throw new Error("All components must be the same length.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = new Uint8Array(byteLen);
|
||||||
|
for (const hex of hexComponents) {
|
||||||
|
for (let i = 0; i < byteLen; i++) {
|
||||||
|
result[i] ^= parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyHex = Array.from(result, b => b.toString(16).padStart(2, "0").toUpperCase()).join("");
|
||||||
|
|
||||||
|
if (!outputJson) return keyHex;
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
algorithm: "XOR",
|
||||||
|
keyLengthBits: byteLen * 8,
|
||||||
|
componentCount: hexComponents.length,
|
||||||
|
keyHex
|
||||||
|
}, null, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyComponentCombine;
|
||||||
128
src/core/operations/KeyComponentSplit.mjs
Normal file
128
src/core/operations/KeyComponentSplit.mjs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
/**
|
||||||
|
* @author Jacob Marks [jacob.marks@jacobmarks.com]
|
||||||
|
* @copyright Jacob Marks 2026
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Operation from "../Operation.mjs";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns cryptographically random bytes.
|
||||||
|
*
|
||||||
|
* @param {number} n
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function randomBytes(n) {
|
||||||
|
const buf = new Uint8Array(n);
|
||||||
|
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
||||||
|
crypto.getRandomValues(buf);
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < n; i++) buf[i] = Math.floor(Math.random() * 256);
|
||||||
|
}
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a Uint8Array to an uppercase hex string.
|
||||||
|
*
|
||||||
|
* @param {Uint8Array} bytes
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function toHex(bytes) {
|
||||||
|
return Array.from(bytes, b => b.toString(16).padStart(2, "0").toUpperCase()).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a hex string to a Uint8Array.
|
||||||
|
*
|
||||||
|
* @param {string} hex
|
||||||
|
* @returns {Uint8Array}
|
||||||
|
*/
|
||||||
|
function hexToBytes(hex) {
|
||||||
|
const out = new Uint8Array(hex.length / 2);
|
||||||
|
for (let i = 0; i < out.length; i++) {
|
||||||
|
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key Component Split operation
|
||||||
|
*/
|
||||||
|
class KeyComponentSplit extends Operation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyComponentSplit constructor
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.name = "Key Component Split";
|
||||||
|
this.module = "Payment";
|
||||||
|
this.description = "Splits a symmetric key into N XOR components for key ceremony use. N-1 components are generated randomly; the final component is derived so that XOR of all N components equals the original key. Accepts 2–8 components. Recombine with Key Component Combine.<br><br>Output is one component per line (hex). Use JSON output to include component count and key length metadata.";
|
||||||
|
this.infoURL = "";
|
||||||
|
this.inputType = "string";
|
||||||
|
this.outputType = "string";
|
||||||
|
this.args = [
|
||||||
|
{
|
||||||
|
name: "Number of components",
|
||||||
|
type: "number",
|
||||||
|
value: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Output as JSON",
|
||||||
|
type: "boolean",
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
this.testDataSamples = [{
|
||||||
|
input: "0123456789ABCDEFFEDCBA9876543210",
|
||||||
|
args: [3, false]
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} input
|
||||||
|
* @param {Object[]} args
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
run(input, args) {
|
||||||
|
const [numComponents, outputJson] = args;
|
||||||
|
|
||||||
|
const keyHex = input.trim().toUpperCase().replace(/\s+/g, "");
|
||||||
|
if (keyHex.length === 0) throw new Error("Input key is empty.");
|
||||||
|
if (!/^[0-9A-F]+$/.test(keyHex) || keyHex.length % 2 !== 0) {
|
||||||
|
throw new Error("Input must be a valid even-length hex string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const n = Math.round(numComponents);
|
||||||
|
if (n < 2 || n > 8) throw new Error("Number of components must be between 2 and 8.");
|
||||||
|
|
||||||
|
const keyBytes = hexToBytes(keyHex);
|
||||||
|
const len = keyBytes.length;
|
||||||
|
|
||||||
|
// Generate N-1 random components; last = key XOR all others
|
||||||
|
const components = [];
|
||||||
|
for (let i = 0; i < n - 1; i++) components.push(randomBytes(len));
|
||||||
|
|
||||||
|
const last = new Uint8Array(keyBytes);
|
||||||
|
for (const c of components) {
|
||||||
|
for (let i = 0; i < len; i++) last[i] ^= c[i];
|
||||||
|
}
|
||||||
|
components.push(last);
|
||||||
|
|
||||||
|
const hexComponents = components.map(toHex);
|
||||||
|
|
||||||
|
if (!outputJson) return hexComponents.join("\n");
|
||||||
|
|
||||||
|
return JSON.stringify({
|
||||||
|
algorithm: "XOR",
|
||||||
|
keyLengthBits: len * 8,
|
||||||
|
componentCount: n,
|
||||||
|
components: hexComponents
|
||||||
|
}, null, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyComponentSplit;
|
||||||
@ -1427,5 +1427,93 @@ TestRegister.addTests([
|
|||||||
"AABBCCDDEEFF00112233445566778899AABBCCDDEEFF0011", "ISO Format 0", "5432101234567890", false]
|
"AABBCCDDEEFF00112233445566778899AABBCCDDEEFF0011", "ISO Format 0", "5432101234567890", false]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Key Component Split / Combine ─────────────────────────────────────────
|
||||||
|
// Vectors: fixed 2-component split using known components so the test is
|
||||||
|
// deterministic. Split is non-deterministic by design so only combine is
|
||||||
|
// tested with known vectors; round-trip is verified via the chain test.
|
||||||
|
// Key : 0123456789ABCDEFFEDCBA9876543210
|
||||||
|
// C1 : FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
||||||
|
// C2 : FEDCBA98765432100123456789ABCDEF (= Key XOR C1)
|
||||||
|
{
|
||||||
|
name: "Key Component Combine: 2-component XOR",
|
||||||
|
input: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\nFEDCBA98765432100123456789ABCDEF",
|
||||||
|
expectedOutput: "0123456789ABCDEFFEDCBA9876543210",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
op: "Key Component Combine",
|
||||||
|
args: [false]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Key Component Combine: 3-component XOR",
|
||||||
|
// C1 XOR C2 XOR C3 = Key
|
||||||
|
// C1: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
||||||
|
// C2: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
|
||||||
|
// C1 XOR C2: 1111111111111111 (repeated)
|
||||||
|
// C3: Key XOR C1 XOR C2 = 0123... XOR 1111... = 10325476 98BADCFE EFCDAB89 67452301
|
||||||
|
// 01^11=10, 23^11=32, 45^11=54, 67^11=76, 89^11=98, AB^11=BA, CD^11=DC, EF^11=FE
|
||||||
|
// FE^11=EF, DC^11=CD, BA^11=AB, 98^11=89, 76^11=67, 54^11=45, 32^11=23, 10^11=01
|
||||||
|
input: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\n10325476 98BADCFE EFCDAB8967452301",
|
||||||
|
expectedOutput: "0123456789ABCDEFFEDCBA9876543210",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
op: "Key Component Combine",
|
||||||
|
args: [false]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Key Component Combine: JSON input from Split",
|
||||||
|
input: JSON.stringify({
|
||||||
|
algorithm: "XOR",
|
||||||
|
keyLengthBits: 128,
|
||||||
|
componentCount: 2,
|
||||||
|
components: [
|
||||||
|
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
|
||||||
|
"FEDCBA98765432100123456789ABCDEF"
|
||||||
|
]
|
||||||
|
}, null, 4),
|
||||||
|
expectedOutput: "0123456789ABCDEFFEDCBA9876543210",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
op: "Key Component Combine",
|
||||||
|
args: [false]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Key Component Combine: JSON output mode",
|
||||||
|
input: "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF\nFEDCBA98765432100123456789ABCDEF",
|
||||||
|
expectedOutput: JSON.stringify({
|
||||||
|
algorithm: "XOR",
|
||||||
|
keyLengthBits: 128,
|
||||||
|
componentCount: 2,
|
||||||
|
keyHex: "0123456789ABCDEFFEDCBA9876543210"
|
||||||
|
}, null, 4),
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
op: "Key Component Combine",
|
||||||
|
args: [true]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Chain: Key Component Split → Combine (round-trip)",
|
||||||
|
input: "0123456789ABCDEFFEDCBA9876543210",
|
||||||
|
expectedOutput: "0123456789ABCDEFFEDCBA9876543210",
|
||||||
|
recipeConfig: [
|
||||||
|
{
|
||||||
|
op: "Key Component Split",
|
||||||
|
args: [3, false]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
op: "Key Component Combine",
|
||||||
|
args: [false]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user