From e864259c1e0f6b941295716611cb10e341a47c97 Mon Sep 17 00:00:00 2001 From: J8k3 Date: Mon, 18 May 2026 21:53:28 -0400 Subject: [PATCH] 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 --- src/core/lib/PaymentPinVerification.mjs | 19 ++++++++++++++----- src/core/operations/VerifyVISAPVV.mjs | 2 +- tests/operations/tests/Payment.mjs | 12 ++++++------ 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/core/lib/PaymentPinVerification.mjs b/src/core/lib/PaymentPinVerification.mjs index d2c699fc..33c419f4 100644 --- a/src/core/lib/PaymentPinVerification.mjs +++ b/src/core/lib/PaymentPinVerification.mjs @@ -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); } diff --git a/src/core/operations/VerifyVISAPVV.mjs b/src/core/operations/VerifyVISAPVV.mjs index 01b6b49c..2fd381d6 100644 --- a/src/core/operations/VerifyVISAPVV.mjs +++ b/src/core/operations/VerifyVISAPVV.mjs @@ -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] } ]; diff --git a/tests/operations/tests/Payment.mjs b/tests/operations/tests/Payment.mjs index bbf66cf6..a2abcfb6 100644 --- a/tests/operations/tests/Payment.mjs +++ b/tests/operations/tests/Payment.mjs @@ -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: [