Two 3DES passes and a decimalization table
"Compute all three" builds two 8-byte blocks — the PAN itself, then the expiry date and service code padded with 0xF — encrypts the first under the Card Verification Key, XORs that result with the second block, encrypts again, and decimalizes the final 8 bytes down to 3 digits.
function computeCvv(pan, expiryYYMM, serviceCode, cvk) {
const block1 = pan.padStart(16, "0").slice(-16); // the PAN itself, as hex nibbles
const block2 = (expiryYYMM + serviceCode).padEnd(16, "F"); // 4 + 3 = 7 digits, then padding
const { k1, k2, k3 } = keyHalves(cvk);
const encrypted1 = tdesEncryptBlock(hexToBigInt(block1), k1, k2, k3);
const xored = hexToBigInt(bigIntToHex(encrypted1, 8)) ^ hexToBigInt(block2);
const encrypted2 = tdesEncryptBlock(xored, k1, k2, k3);
return decimalize(bigIntToHex(encrypted2, 8), 3);
}Every decimal digit of the PAN doubles as one hex nibble directly — no separate encoding step, since PAN digits are already 0–9.
Decimalization: a fixed, public table
3DES output is 16 hex digits, but a CVV has to be entirely numeric. Decimalization maps each hex digit straight across for 0–9, and folds A–F down onto 0–5 — a fixed, publicly known transformation. None of the security comes from keeping this table secret; all of it comes from the CVK never leaving the issuer's HSM.
const DECIMALIZE = {
0:"0",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7",8:"8",9:"9",
A:"0", B:"1", C:"2", D:"3", E:"4", F:"5",
};
function decimalize(hex, digits) {
return hex.split("").map(c => DECIMALIZE[c]).join("").slice(0, digits);
}Only the first 3 decimalized digits become the printed CVV — the rest of the 3DES output is simply discarded, which is also why a CVV alone can't be reversed back into a key or a PAN.
Why CVV1, CVV2, and iCVV never cross-validate
The "Compute all three" button runs the identical function three times, changing only the 3-digit service code: 101 for CVV1 (magstripe), 000 for CVV2 (printed, card-not-present), and 999 for iCVV (chip). Because the service code feeds directly into block 2 before the second encryption, a one-digit change there cascades into a completely unrelated final cryptogram — there's no partial credit, no shared prefix, nothing that links the three values except that they came from the same PAN, expiry, and CVK.