Add PIN Generate op: random PIN with optional clear PIN block output

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
J8k3 2026-05-19 10:16:19 -04:00
parent af84b44117
commit 8ac6cc1980
3 changed files with 198 additions and 2 deletions

View File

@ -599,8 +599,6 @@
"EMV Verify MAC",
"HSM Parse Futurex Command",
"HSM Parse Thales Command",
"PIN IBM 3624 Offset Generate",
"PIN IBM 3624 Verify",
"Key Generate",
"MAC Generate",
"MAC Verify",
@ -615,6 +613,9 @@
"PIN Block Translate",
"PIN Data Generate",
"PIN Data Verify",
"PIN Generate",
"PIN IBM 3624 Offset Generate",
"PIN IBM 3624 Verify",
"TR-31 Parse Key Block",
"TR-34 Parse Key Transport",
"VISA PVV Generate",

View File

@ -0,0 +1,125 @@
/**
* @license Apache-2.0
* @author Jacob Marks [https://jacobmarks.com]
*/
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { buildPinBlock } from "../lib/PinBlock.mjs";
const PIN_OUTPUT_MODES = [
"PIN digits",
"ISO Format 0 clear PIN block",
"ISO Format 1 clear PIN block",
"ISO Format 3 clear PIN block",
];
// Maps output mode label → PIN_BLOCK_FORMATS string used by buildPinBlock
const OUTPUT_TO_FORMAT = {
"ISO Format 0 clear PIN block": "ISO Format 0",
"ISO Format 1 clear PIN block": "ISO Format 1",
"ISO Format 3 clear PIN block": "ISO Format 3",
};
/**
* Generate PIN operation.
*/
class GeneratePIN extends Operation {
/**
* GeneratePIN constructor.
*/
constructor() {
super();
this.name = "PIN Generate";
this.module = "Payment";
this.description = "Generate a cryptographically random cardholder PIN and optionally encode it as a clear ISO 9564 PIN block for use in test recipes.<br><br><b>Input:</b> ignored.<br><b>Arguments:</b> choose the PIN length, the output mode, and (for block modes) the PAN.<br><br>The PIN digits are drawn using <code>crypto.getRandomValues</code> with rejection sampling to guarantee uniform distribution across 09.<br><br>Block output modes produce the clear (unencrypted) PIN block directly; these are test artifacts and must not be treated as production PIN blocks.<br><br><b>Security:</b> Test data only. Do not use generated PINs or clear PIN blocks in production systems.";
this.inlineHelp = "<strong>Input:</strong> ignored.<br><strong>Args:</strong> PIN length, output mode, and PAN for block formats.<br><strong>Validation:</strong> uniform random digits via crypto.getRandomValues; clear ISO 9564 block formats 0, 1, and 3.";
this.testDataSamples = [
{
name: "4-digit PIN, digits only",
input: "",
args: [4, "PIN digits", ""]
},
{
name: "4-digit PIN, Format 0 block",
input: "",
args: [4, "ISO Format 0 clear PIN block", "5432101234567890"]
}
];
this.infoURL = "https://en.wikipedia.org/wiki/Personal_identification_number";
this.inputType = "string";
this.outputType = "string";
this.args = [
{
name: "PIN length",
type: "number",
value: 4,
min: 4,
max: 12,
comment: "Number of PIN digits to generate. Most cardholder PINs are 4 digits."
},
{
name: "Output",
type: "option",
value: PIN_OUTPUT_MODES,
comment: "PIN digits only, or a clear ISO 9564 PIN block. Block modes require the PAN argument."
},
{
name: "PAN (for block formats)",
type: "string",
value: "",
comment: "Required for ISO Format 0 and Format 3 block output. Ignored for PIN digits and ISO Format 1."
}
];
}
/**
* @param {string} input
* @param {Object[]} args
* @returns {string}
*/
run(input, args) {
const [length, outputMode, pan] = args;
if (!Number.isInteger(length) || length < 4 || length > 12) {
throw new OperationError("PIN length must be between 4 and 12.");
}
const pin = generateRandomPin(length);
if (outputMode === "PIN digits") return pin;
const format = OUTPUT_TO_FORMAT[outputMode];
return buildPinBlock(format, pin, pan, true);
}
}
/**
* Generates a single random decimal digit using rejection sampling.
* Rejects values >= 250 to ensure uniform distribution across 09
* (250 = 25 × 10, so bytes 0249 map to exactly 25 values per digit).
*
* @returns {number}
*/
function randomDecimalDigit() {
const buf = new Uint8Array(1);
let b;
do {
globalThis.crypto.getRandomValues(buf);
b = buf[0];
} while (b >= 250);
return b % 10;
}
/**
* Generates a random PIN of the given length.
*
* @param {number} length
* @returns {string}
*/
function generateRandomPin(length) {
return Array.from({ length }, () => randomDecimalDigit()).join("");
}
export default GeneratePIN;

View File

@ -848,6 +848,76 @@ TestRegister.addTests([
}
]
},
{
name: "PIN Generate: 4-digit PIN digits",
input: "",
expectedMatch: /^\d{4}$/,
recipeConfig: [
{
op: "PIN Generate",
args: [4, "PIN digits", ""]
}
]
},
{
name: "PIN Generate: 6-digit PIN digits",
input: "",
expectedMatch: /^\d{6}$/,
recipeConfig: [
{
op: "PIN Generate",
args: [6, "PIN digits", ""]
}
]
},
{
name: "PIN Generate: ISO Format 0 block",
input: "",
expectedMatch: /^[0-9A-F]{16}$/,
recipeConfig: [
{
op: "PIN Generate",
args: [4, "ISO Format 0 clear PIN block", "5432101234567890"]
}
]
},
{
name: "PIN Generate: ISO Format 1 block",
input: "",
expectedMatch: /^[0-9A-F]{16}$/,
recipeConfig: [
{
op: "PIN Generate",
args: [4, "ISO Format 1 clear PIN block", ""]
}
]
},
{
name: "PIN Generate: ISO Format 3 block",
input: "",
expectedMatch: /^[0-9A-F]{16}$/,
recipeConfig: [
{
op: "PIN Generate",
args: [4, "ISO Format 3 clear PIN block", "5432101234567890"]
}
]
},
{
name: "Chain: PIN Generate → PIN Data Generate (Format 0)",
input: "",
expectedMatch: /^[0-9A-F]{16}$/,
recipeConfig: [
{
op: "PIN Generate",
args: [4, "PIN digits", ""]
},
{
op: "PIN Data Generate",
args: ["ISO Format 0", "5432101234567890", false, false]
}
]
},
{
name: "PIN IBM 3624 Offset Generate: known sample",
input: "1234",