diff --git a/src/core/operations/DeriveDUKPTAESKey.mjs b/src/core/operations/DeriveDUKPTAESKey.mjs
index c061cd7e..0d2d8217 100644
--- a/src/core/operations/DeriveDUKPTAESKey.mjs
+++ b/src/core/operations/DeriveDUKPTAESKey.mjs
@@ -11,7 +11,7 @@ import { toHexFast } from "../lib/Hex.mjs";
// ── X9.24-3 key usage indicators (bytes 2-3 of derivation data) ───────────────
const KEY_USAGE = {
- "IK Derivation": 0x8000, // BDK → device Initial Key
+ "IK Derivation": 0x8001, // BDK → device Initial Key (X9.24-3 §6.3.1)
Intermediate: 0x0000, // internal binary-tree node (not user-visible)
"PIN Encryption": 0x1000,
"MAC Generation": 0x2000, // sender / request direction
@@ -165,15 +165,42 @@ function aesCmac(key16, message) {
// ── X9.24-3 AES-128 DUKPT derivation ─────────────────────────────────────────
/**
- * Builds the 20-byte derivation data block (ANSI X9.24-3-2017).
+ * Builds the 16-byte IK derivation data block (ANSI X9.24-3-2017 §6.3.1).
+ *
+ * Layout (IK derivation only — uses full 8-byte IKI, no counter field):
+ * [0] version = 0x01
+ * [1] key size class = 0x01 (AES-128)
+ * [2-3] key usage = 0x8001 (IK Derivation)
+ * [4-5] algorithm = 0x0002 (AES-128)
+ * [6-7] key length = 0x0080 (128 bits)
+ * [8-15] IKI (full 8 bytes)
+ *
+ * @param {Uint8Array} iki8
+ * @returns {Uint8Array}
+ */
+function ikDerivationData(iki8) {
+ const d = new Uint8Array(16);
+ d[0] = 0x01; d[1] = 0x01;
+ d[2] = (KEY_USAGE["IK Derivation"] >> 8) & 0xFF;
+ d[3] = KEY_USAGE["IK Derivation"] & 0xFF;
+ d[4] = (ALGO_CODE >> 8) & 0xFF; d[5] = ALGO_CODE & 0xFF;
+ d[6] = (KEY_LEN_VAL >> 8) & 0xFF; d[7] = KEY_LEN_VAL & 0xFF;
+ d.set(iki8, 8);
+ return d;
+}
+
+/**
+ * Builds the 16-byte derivation data block for intermediate-node and working-key
+ * derivation (ANSI X9.24-3-2017 §6.3.2 / §6.3.3).
*
* Layout:
- * [0-1] version = 0x0001
- * [2-3] key usage indicator
- * [4-5] algorithm = 0x0002 (AES-128)
- * [6-7] key length = 0x0080 (128 bits)
- * [8-15] IKI (8 bytes, from KSN bytes 0-7)
- * [16-19] counter register (4 bytes)
+ * [0] version = 0x01
+ * [1] key size class = 0x01 (AES-128)
+ * [2-3] key usage indicator
+ * [4-5] algorithm = 0x0002 (AES-128)
+ * [6-7] key length = 0x0080 (128 bits)
+ * [8-11] last 4 bytes of IKI (IKI[4..7])
+ * [12-15] counter register (4 bytes, big-endian)
*
* @param {number} usage
* @param {Uint8Array} iki8
@@ -181,16 +208,17 @@ function aesCmac(key16, message) {
* @returns {Uint8Array}
*/
function derivationData(usage, iki8, counterReg) {
- const d = new Uint8Array(20);
- d[0] = 0x00; d[1] = 0x01;
+ const d = new Uint8Array(16);
+ d[0] = 0x01; d[1] = 0x01;
d[2] = (usage >> 8) & 0xFF; d[3] = usage & 0xFF;
d[4] = (ALGO_CODE >> 8) & 0xFF; d[5] = ALGO_CODE & 0xFF;
d[6] = (KEY_LEN_VAL >> 8) & 0xFF; d[7] = KEY_LEN_VAL & 0xFF;
- d.set(iki8, 8);
- d[16] = (counterReg >>> 24) & 0xFF;
- d[17] = (counterReg >>> 16) & 0xFF;
- d[18] = (counterReg >>> 8) & 0xFF;
- d[19] = counterReg & 0xFF;
+ // last 4 bytes of 8-byte IKI
+ d[8] = iki8[4]; d[9] = iki8[5]; d[10] = iki8[6]; d[11] = iki8[7];
+ d[12] = (counterReg >>> 24) & 0xFF;
+ d[13] = (counterReg >>> 16) & 0xFF;
+ d[14] = (counterReg >>> 8) & 0xFF;
+ d[15] = counterReg & 0xFF;
return d;
}
@@ -202,7 +230,7 @@ function derivationData(usage, iki8, counterReg) {
* @returns {Uint8Array}
*/
function deriveIK(bdk16, iki8) {
- return aesCmac(bdk16, derivationData(KEY_USAGE["IK Derivation"], iki8, 0));
+ return aesCmac(bdk16, ikDerivationData(iki8));
}
/**
@@ -235,6 +263,7 @@ function deriveTransactionKey(ik16, iki8, counter) {
/**
* Derives a purpose-specific working key from the transaction key.
+ * Uses the full 32-bit counter in the derivation data (not the 21-bit tree counter).
*
* @param {Uint8Array} txKey16
* @param {Uint8Array} iki8
@@ -243,7 +272,7 @@ function deriveTransactionKey(ik16, iki8, counter) {
* @returns {Uint8Array}
*/
function deriveWorkingKey(txKey16, iki8, counter, purposeName) {
- return aesCmac(txKey16, derivationData(KEY_USAGE[purposeName], iki8, counter & 0x1FFFFF));
+ return aesCmac(txKey16, derivationData(KEY_USAGE[purposeName], iki8, counter));
}
// ── Operation class ───────────────────────────────────────────────────────────
@@ -269,15 +298,17 @@ class DeriveDUKPTAESKey extends Operation {
"The KSN is 12 bytes: 8-byte Initial Key Identifier (IKI) + 4-byte transaction counter.",
"Only the low 21 bits of the counter are used for derivation (max 2,097,151 transactions per IK).",
"
",
- "Derivation data format (X9.24-3, 20 bytes):",
+ "Derivation data format (X9.24-3, 16 bytes — working keys):",
"
",
- "[0-1] version = 0x0001\n",
+ "[0] version = 0x01\n",
+ "[1] key size class = 0x01 (AES-128)\n",
"[2-3] key usage indicator\n",
"[4-5] algorithm = 0x0002 (AES-128)\n",
"[6-7] key length = 0x0080 (128 bits)\n",
- "[8-15] IKI (8 bytes from KSN)\n",
- "[16-19] counter register (4 bytes)\n",
+ "[8-11] last 4 bytes of IKI\n",
+ "[12-15] transaction counter (4 bytes, full 32-bit value)\n",
"",
+ "IK derivation uses a separate 16-byte block with full 8-byte IKI at [8-15] and usage 0x8001.",
"Key usage codes: PIN Encryption=0x1000, MAC Generation=0x2000, ",
"MAC Verification=0x2001, MAC Both Ways=0x2002, ",
"Data Encryption=0x3000, Data Decryption=0x3001, Data Both Ways=0x3002.",
diff --git a/tests/operations/tests/Payment.mjs b/tests/operations/tests/Payment.mjs
index a2abcfb6..0307944f 100644
--- a/tests/operations/tests/Payment.mjs
+++ b/tests/operations/tests/Payment.mjs
@@ -315,6 +315,98 @@ TestRegister.addTests([
}
]
},
+ {
+ // ── DUKPT Derive AES Key — ANSI X9.24-3-2017 official test vectors ───────
+ // BDK-128: FEDCBA9876543210F1F1F1F1F1F1F1F1
+ // KSN: 1234567890123456 (IKI) + counter
+ // Source: https://x9.org/standards/x9-24-part-3-test-vectors/
+ name: "DUKPT Derive AES Key: IK from BDK (X9.24-3 §6.3.1)",
+ input: "FEDCBA9876543210F1F1F1F1F1F1F1F1",
+ expectedOutput: "1273671EA26AC29AFA4D1084127652A1",
+ recipeConfig: [
+ {
+ op: "DUKPT Derive AES Key",
+ args: ["BDK", "Initial Key (IK)", "123456789012345600000001", "PIN Encryption", false]
+ }
+ ]
+ },
+ {
+ name: "DUKPT Derive AES Key: PIN Encryption key, counter 1 (X9.24-3 §6.3.3)",
+ input: "FEDCBA9876543210F1F1F1F1F1F1F1F1",
+ expectedOutput: "AF8CB133A78F8DC2D1359F18527593FB",
+ recipeConfig: [
+ {
+ op: "DUKPT Derive AES Key",
+ args: ["BDK", "Working Key", "123456789012345600000001", "PIN Encryption", false]
+ }
+ ]
+ },
+ {
+ name: "DUKPT Derive AES Key: MAC Generation key, counter 1",
+ input: "FEDCBA9876543210F1F1F1F1F1F1F1F1",
+ expectedOutput: "A2DC23DE6FDE0824A2BC321E08E4B8B7",
+ recipeConfig: [
+ {
+ op: "DUKPT Derive AES Key",
+ args: ["BDK", "Working Key", "123456789012345600000001", "MAC Generation", false]
+ }
+ ]
+ },
+ {
+ name: "DUKPT Derive AES Key: Data Encryption key, counter 1",
+ input: "FEDCBA9876543210F1F1F1F1F1F1F1F1",
+ expectedOutput: "A35C412EFD41FDB98B69797C02DCD08F",
+ recipeConfig: [
+ {
+ op: "DUKPT Derive AES Key",
+ args: ["BDK", "Working Key", "123456789012345600000001", "Data Encryption", false]
+ }
+ ]
+ },
+ {
+ name: "DUKPT Derive AES Key: PIN Encryption key, counter 8",
+ input: "FEDCBA9876543210F1F1F1F1F1F1F1F1",
+ expectedOutput: "4D9DF3FBEE3448FC3E676D04320A90F5",
+ recipeConfig: [
+ {
+ op: "DUKPT Derive AES Key",
+ args: ["BDK", "Working Key", "123456789012345600000008", "PIN Encryption", false]
+ }
+ ]
+ },
+ {
+ name: "DUKPT Derive AES Key: PIN Encryption key, counter 131072 (0x20000, first skipped-bit counter)",
+ input: "FEDCBA9876543210F1F1F1F1F1F1F1F1",
+ expectedOutput: "AB828BE7B58C7EC5D5ED0D5D320A0C9D",
+ recipeConfig: [
+ {
+ op: "DUKPT Derive AES Key",
+ args: ["BDK", "Working Key", "123456789012345600020000", "PIN Encryption", false]
+ }
+ ]
+ },
+ {
+ name: "DUKPT Derive AES Key: PIN Encryption key, counter 8675309 (0x845FED, midrange)",
+ input: "FEDCBA9876543210F1F1F1F1F1F1F1F1",
+ expectedOutput: "D1DDA386AA4A556AF0119FDCB5D132C6",
+ recipeConfig: [
+ {
+ op: "DUKPT Derive AES Key",
+ args: ["BDK", "Working Key", "1234567890123456 00845FED", "PIN Encryption", false]
+ }
+ ]
+ },
+ {
+ name: "DUKPT Derive AES Key: working key from IK input, counter 1 PIN Encryption",
+ input: "1273671EA26AC29AFA4D1084127652A1",
+ expectedOutput: "AF8CB133A78F8DC2D1359F18527593FB",
+ recipeConfig: [
+ {
+ op: "DUKPT Derive AES Key",
+ args: ["Initial Key (IK)", "Working Key", "123456789012345600000001", "PIN Encryption", false]
+ }
+ ]
+ },
{
name: "DUKPT Derive TDES Key: known IPEK vector",
input: "0123456789ABCDEFFEDCBA9876543210",