The problem DUKPT solves
A merchant's PIN pad has to encrypt every PIN it captures, but it sits on a shop counter where it can be stolen and taken apart. If it held one long-lived key, breaking one device would expose every PIN it had ever encrypted. Derived Unique Key Per Transaction (DUKPT, ANSI X9.24) gives every transaction its own key and makes the device forget each key after use, so what an attacker extracts from a captured device can't decrypt anything it sent before.
The acquirer's host doesn't store a key per device either. It holds one Base Derivation Key (BDK) in its HSM, and each message carries the device's Key Serial Number (KSN). From the BDK and the KSN alone the host re-derives exactly the key the device used. The BDK itself never goes near a device.
The Key Serial Number
In TDES DUKPT the KSN is 10 bytes: a 59-bit initial KSN (a key set identifier naming the BDK, plus the device's own ID) and a 21-bit transaction counter. In AES DUKPT it's 12 bytes: a 4-byte BDK ID, a 4-byte derivation ID (together the 8-byte initial key ID), and a full 32-bit counter. The tool splits whichever KSN you enter into those fields.
A device never uses a counter with too many one bits: at most 10 for TDES, 16 for AES. That bound caps how many intermediate keys the device has to keep (21 or 32 registers, one per bit) and how many derivation steps the host needs (at most 10 or 16). 'Next transaction' skips the invalid values the way a device does, which is why the counter jumps from 0x0FF802 to 0x0FF804. That leaves 1,048,575 usable counters for TDES (the last is 0x1FF800) and about 2.4 billion for AES (the last is 0xFFFF0000).
TDES DUKPT: IPEK and the non-reversible key generation process
The Initial PIN Encryption Key (IPEK) is two TDES encryptions of the initial KSN's leftmost 8 bytes: one under the BDK for the left half, one under the BDK XORed with C0C0C0C000000000C0C0C0C000000000 for the right half. That IPEK is what gets injected into the device.
To reach a given counter, start from the IPEK and walk the counter's set bits from most significant to least. At each bit, set it in a KSN register and apply X9.24-1's non-reversible key generation process: XOR the register with the key's right half, single-DES encrypt under the left half, XOR the right half again. Doing that once with the key and once with the key XORed with the C0C0… mask gives the two halves of the next key. The tool lists every register value and intermediate key; counter 0x15 takes three steps (bits 0x10, 0x04, 0x01).
Working keys are XOR variants of the transaction key: …00FF in bytes 7 and 15 for PIN encryption, …FF00 for request MACs, and so on. The two data-encryption variants also go through a one-way function (each half TDES-encrypted under the variant itself), so the variant is never used directly.
export function tdesNonReversible(keyHex: string, registerHex: string): string {
const half = (k: string) =>
xorHex(des(k.slice(0, 16), xorHex(registerHex, k.slice(16))), k.slice(16));
return half(xorHex(keyHex, C0_MASK)) + half(keyHex);
}des() is single DES from the site's FIPS 46-3 implementation; the IPEK uses 2-key TDES from the same file.
Applied under K ⊕ C0C0… for the new left half and under K for the new right half.
AES DUKPT: one derivation function for everything
X9.24-3:2017 replaces the variants and the C0C0 mask with a single construction. Every key, whether the initial key, an intermediate key, or a working key, is AES-ECB under its parent key of a 16-byte derivation data block: version 01, a block counter, a 2-byte key usage, a 2-byte algorithm, the derived key's length in bits, and 8 bytes of KSN data. A 256-bit key takes two AES calls, with block counters 01 and 02.
The usage field does the key separation that TDES did with variants: 8001 for the initial key, 8000 for intermediate keys, 1000 for PIN encryption, 2000 and 2001 for MAC generation and verification, 3000 and 3001 for data encryption and decryption. The algorithm field names what the new key is for (0002 AES-128, 0004 AES-256, 0000 and 0001 for 2- and 3-key TDES), so an AES BDK can still produce TDES working keys for older host links. A working key may never be stronger than the key it comes from: an AES-128 BDK can't yield an AES-256 working key.
const ksnData = usage === AES_KEY_USAGES.initialKey
? initialKeyId // all 8 bytes
: initialKeyId.slice(8) + h(counter >>> 0, 8); // derivation ID ‖ counter
return "01" + h(block, 2) + h(usage, 4) + h(code, 4) + h(bits, 4) + ksnData;AES-ECB isn't offered by Web Crypto, so each block is AES-CBC with a zero IV, keeping the first 16 bytes of output.
Where the crypto comes from, and how it's checked
TDES DUKPT runs on the site's from-scratch DES/3DES, since no browser exposes DES. AES DUKPT runs on Web Crypto. Unit tests check both against the published test data: X9.24-1:2009 Annex A.4 (the IPEK, 34 transaction keys across the initial and rollover sequences, and for each of the first 21 transactions the encrypted PIN block, the request and response retail MACs, and the encrypted request, which between them exercise every variant) and the X9.24-3:2017 supplement test vectors for AES-128 and AES-256 BDKs (initial keys, 15 derivation keys each including the last counters before exhaustion, and AES-128 and AES-256 PIN, MAC, and data keys). Every expected value was first reproduced with a separate Python implementation.
This is the host side of DUKPT. A real PIN pad keeps a small table of future keys and erases each one after use rather than re-deriving from the IPEK, but it arrives at the same keys. And like the PIN block tool, this one shows you the BDK and every derived key in the clear; a real HSM never returns any of them.