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:
parent
af84b44117
commit
8ac6cc1980
@ -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",
|
||||
|
||||
125
src/core/operations/GeneratePIN.mjs
Normal file
125
src/core/operations/GeneratePIN.mjs
Normal 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 0–9.<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 0–9
|
||||
* (250 = 25 × 10, so bytes 0–249 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;
|
||||
@ -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",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user