Fix VISA PVV decimalization: use two-pass algorithm per Visa spec

decimalizePvv() was using a single-pass that immediately mapped A-F to
0-5. The Visa PVV spec (matching ANSI X9.8 and jPOS behavior) requires
two-pass: collect all decimal digits (0-9) first; only then re-scan
mapping A=0 B=1 C=2 D=3 E=4 F=5. This matches decimalizeCvvHex() which
was already correct. Bug produced wrong PVV whenever a hex letter appeared
before the first decimal digit in the encrypted output.

Test vectors updated from "6077" (single-pass result) to "6776" (correct
two-pass result) for the encrypted PVV hex 6A77E65CFE349D60.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
J8k3 2026-05-18 21:53:28 -04:00
parent a2880a4dfc
commit e864259c1e
3 changed files with 21 additions and 12 deletions

View File

@ -168,21 +168,30 @@ function verifyIbm3624Pin(pvkHex, decimalizationTable, pinValidationData, padCha
}
/**
* Decimalizes a PVV candidate using the common numeric-first rule.
* Decimalizes a PVV candidate using the standard two-pass rule:
* pass 1 collects decimal digits (0-9); pass 2 maps A=0 B=1 C=2 D=3 E=4 F=5.
*
* @param {string} hex
* @returns {string}
*/
function decimalizePvv(hex) {
const upper = hex.toUpperCase();
let out = "";
for (const ch of hex.toUpperCase()) {
for (const ch of upper) {
if (/\d/.test(ch)) {
out += ch;
} else {
out += String((ch.charCodeAt(0) - "A".charCodeAt(0)) % 10);
if (out.length >= 4) return out.substring(0, 4);
}
if (out.length >= 4) return out.substring(0, 4);
}
for (const ch of upper) {
if (/[A-F]/.test(ch)) {
out += String(ch.charCodeAt(0) - "A".charCodeAt(0));
if (out.length >= 4) return out.substring(0, 4);
}
}
return out.substring(0, 4);
}

View File

@ -23,7 +23,7 @@ class VerifyVISAPVV extends Operation {
this.testDataSamples = [
{
name: "VISA PVV verify sample",
input: "6077",
input: "6776",
args: ["0123456789ABCDEFFEDCBA9876543210", "5432101234567890", 1, "1234", true]
}
];

View File

@ -812,7 +812,7 @@ TestRegister.addTests([
pin: "1234",
pvvInput: "1012345678911234",
encryptedPvvInputHex: "6A77E65CFE349D60",
pvv: "6077"
pvv: "6776"
}, null, 4),
recipeConfig: [
{
@ -823,7 +823,7 @@ TestRegister.addTests([
},
{
name: "VISA PVV Verify: known sample",
input: "6077",
input: "6776",
expectedOutput: JSON.stringify({
pinVerificationKeyHex: "0123456789ABCDEFFEDCBA9876543210",
pan: "5432101234567890",
@ -831,8 +831,8 @@ TestRegister.addTests([
pin: "1234",
pvvInput: "1012345678911234",
encryptedPvvInputHex: "6A77E65CFE349D60",
pvv: "6077",
expectedPvv: "6077",
pvv: "6776",
expectedPvv: "6776",
valid: true
}, null, 4),
recipeConfig: [
@ -913,8 +913,8 @@ TestRegister.addTests([
pin: "1234",
pvvInput: "1012345678911234",
encryptedPvvInputHex: "6A77E65CFE349D60",
pvv: "6077",
expectedPvv: "6077",
pvv: "6776",
expectedPvv: "6776",
valid: true
}, null, 4),
recipeConfig: [