Why this tool can't use crypto.subtle
Every other tool on this site calls the browser's own Web Crypto API. There's no crypto.subtle.encrypt call for DES or 3DES — no browser has ever implemented it, because both are retired for general use. But ISO 9564 PIN blocks are, in real deployed payment systems today, still 3DES end to end. Demonstrating that honestly means running a real 3DES implementation, written from scratch for this site and verified against the official FIPS 46 test vector, rather than silently swapping in AES.
// There is no crypto.subtle.encrypt({ name: "3DES" }, ...) — this API doesn't exist.
// des.ts implements the algorithm directly: the same Initial Permutation, Feistel
// rounds, S-boxes, and key schedule that FIPS 46-3 specifies, operating on 64-bit
// blocks represented as JavaScript BigInts.
import { tdesEncryptBlock, tdesDecryptBlock } from "@/lib/des";Verified against the official test vector: key 133457799BBCDFF1, plaintext 0123456789ABCDEF → ciphertext 85E813540F0AB405 — the same result any conformant DES implementation produces.
Building the ISO-0 block
"Build PIN block" packs the PIN into the fixed 16-hex-digit layout (control nibble, length nibble, PIN digits, 0xF padding), builds the PAN field from the card's own rightmost 12 digits excluding the check digit, and XORs the two together — the construction covered in detail on the PIN block use case page.
function buildPinField(pin) {
const lenNibble = pin.length.toString(16).toUpperCase();
return "0" + lenNibble + pin + "F".repeat(14 - pin.length);
}
function buildPanField(pan) {
const withoutCheckDigit = pan.slice(0, -1);
const last12 = withoutCheckDigit.slice(-12).padStart(12, "0");
return "0000" + last12;
}
const clearBlock = hexToBigInt(pinField) ^ hexToBigInt(panField);Plain bitwise XOR on two BigInts — the same operation, at the same bit width, that a real HSM's firmware performs on dedicated hardware.
Encrypting under the TPK
"Encrypt" runs the clear PIN block through 3DES under the demo Terminal PIN Key — a double-length key, meaning the leftmost 16 hex digits and rightmost 16 hex digits are used as two separate DES keys (k1 and k2) in an Encrypt-Decrypt-Encrypt chain, with k1 reused as the third key. This is what actually leaves a real PIN pad; the clear block from the previous step never does.
function keyHalves(keyHex) {
const k1 = hexToBigInt(keyHex.slice(0, 16));
const k2 = hexToBigInt(keyHex.slice(16, 32));
return { k1, k2, k3: k1 }; // double-length: k3 = k1
}
const { k1, k2, k3 } = keyHalves(tpk);
const encrypted = tdesEncryptBlock(clearBlock, k1, k2, k3);
// internally: DES_encrypt(k1) → DES_decrypt(k2) → DES_encrypt(k3)Encrypt-Decrypt-Encrypt with k3 = k1 is exactly what makes a double-length 3DES key backward-compatible with single DES when k1 happens to equal k2 — a real, if rarely used, escape hatch some legacy systems still rely on.
Translating to a second key
"Translate" decrypts under the TPK and immediately re-encrypts under the ZPK — the one operation a payment HSM exists to perform, because it's the one moment the PIN block is decrypted at all between the terminal and the issuer. This tool shows you the intermediate clear value so you can see what's happening; a real HSM never returns it to the calling application — the decrypted block exists only inside the HSM's own protected memory, for the duration of one function call.
const decrypted = tdesDecryptBlock(encryptedUnderTpk, tpk.k1, tpk.k2, tpk.k3);
const reEncrypted = tdesEncryptBlock(decrypted, zpk.k1, zpk.k2, zpk.k3);
// a real HSM's API returns only reEncrypted — "decrypted" never leaves its boundaryThis tool's own code does return the intermediate value, deliberately, so you can see what a real HSM's internals compute — that's the one place this demo is less strict than production for teaching purposes.