From 71b0a9871e820f7b3f7aeb4c13aa6ef8670cbdcf Mon Sep 17 00:00:00 2001 From: J8k3 Date: Sat, 25 Apr 2026 09:39:46 -0400 Subject: [PATCH] Add AWS-style payment wrapper operations --- AWS_PAYMENT_CRYPTOGRAPHY_RECIPES.md | 83 ++++--- PAYMENT_RECIPES.md | 42 +++- src/core/config/Categories.json | 9 +- src/core/lib/PaymentDataCipher.mjs | 209 ++++++++++++++++++ src/core/operations/DecryptPaymentData.mjs | 54 +++++ src/core/operations/EncryptPaymentData.mjs | 54 +++++ .../operations/GeneratePaymentPINData.mjs | 54 +++++ src/core/operations/ReEncryptPaymentData.mjs | 70 ++++++ .../operations/TranslatePaymentPINData.mjs | 52 +++++ src/core/operations/VerifyEMVARQC.mjs | 56 +++++ src/core/operations/VerifyPaymentPINData.mjs | 56 +++++ tests/operations/tests/Payment.mjs | 108 +++++++++ 12 files changed, 812 insertions(+), 35 deletions(-) create mode 100644 src/core/lib/PaymentDataCipher.mjs create mode 100644 src/core/operations/DecryptPaymentData.mjs create mode 100644 src/core/operations/EncryptPaymentData.mjs create mode 100644 src/core/operations/GeneratePaymentPINData.mjs create mode 100644 src/core/operations/ReEncryptPaymentData.mjs create mode 100644 src/core/operations/TranslatePaymentPINData.mjs create mode 100644 src/core/operations/VerifyEMVARQC.mjs create mode 100644 src/core/operations/VerifyPaymentPINData.mjs diff --git a/AWS_PAYMENT_CRYPTOGRAPHY_RECIPES.md b/AWS_PAYMENT_CRYPTOGRAPHY_RECIPES.md index 1edfdcc2..ebca87b2 100644 --- a/AWS_PAYMENT_CRYPTOGRAPHY_RECIPES.md +++ b/AWS_PAYMENT_CRYPTOGRAPHY_RECIPES.md @@ -22,18 +22,18 @@ Coverage legend: | AWS operation | Coverage | Notes | | --- | --- | --- | -| `EncryptData` | `Direct` / `Partial` | Direct for AES, TDES, RSA. Partial for DUKPT and EMV-derived encryption. | -| `DecryptData` | `Direct` / `Partial` | Direct for AES, TDES, RSA. Partial for DUKPT and EMV-derived decryption. | -| `ReEncryptData` | `Direct` / `Partial` | Direct for plain decrypt-then-encrypt workflows. Partial for DUKPT re-encryption. | +| `EncryptData` | `Direct` / `Partial` | Direct for AES, TDES, and the implemented DUKPT-TDES wrapper profiles. Partial for EMV-derived encryption and broader AWS attribute coverage. | +| `DecryptData` | `Direct` / `Partial` | Direct for AES, TDES, and the implemented DUKPT-TDES wrapper profiles. Partial for EMV-derived decryption and broader AWS attribute coverage. | +| `ReEncryptData` | `Direct` / `Partial` | Direct for plain decrypt-then-encrypt workflows, including the implemented payment-facing AES/TDES wrapper flows. Partial for DUKPT re-encryption breadth and AWS-specific metadata handling. | | `GenerateMac` | `Direct` / `Partial` | Direct for static-key HMAC and CMAC, and direct for the implemented DUKPT CMAC wrapper modes. Partial for ISO 9797, EMV MAC, and AS2805 flows. | | `VerifyMac` | `Direct` / `Partial` | Direct for static-key HMAC and CMAC, and direct for the implemented DUKPT CMAC wrapper modes. Partial for ISO 9797, EMV MAC, and AS2805 flows. | -| `VerifyAuthRequestCryptogram` | `Partial` | Usable for AES-CMAC ARQC/ARPC-style checking when session key and preimage are already known. Dedicated ARQC and ARPC generators now exist for that constrained profile. | +| `VerifyAuthRequestCryptogram` | `Partial` | Usable for AES-CMAC ARQC/ARPC-style checking when session key and preimage are already known. Dedicated ARQC, ARPC, and ARQC verify wrappers now exist for that constrained profile. | | `TranslateKeyMaterial` | `Partial` | Useful for ECDH derivation and TR-31 inspection, not full HSM-side rewrap semantics. | | `GenerateCardValidationData` | `Direct` | Direct for software CVV/CVV2/iCVV generation when the combined CVK pair is provided as clear hex. | | `VerifyCardValidationData` | `Direct` | Direct for software CVV/CVV2/iCVV verification using the same clear-CVK assumptions as generation. | -| `GeneratePinData` | `Partial` | Clear PIN-block build coverage now exists for ISO formats 0, 1, and 3. PVV, IBM3624, and encrypted-generation paths are still missing. | -| `TranslatePinData` | `Partial` | Clear PIN-block parse and translate coverage now exists for ISO formats 0, 1, and 3. Encrypted PEK/BDK/ECDH translation is still missing. | -| `VerifyPinData` | `Partial` | Clear PIN-block decoding exists, but PVV / IBM3624 verification behavior is still missing. | +| `GeneratePinData` | `Partial` | Clear PIN-block wrapper coverage now exists for ISO formats 0, 1, and 3. PVV, IBM3624, and encrypted-generation paths are still missing. | +| `TranslatePinData` | `Partial` | Clear PIN-block wrapper coverage now exists for ISO formats 0, 1, and 3. Encrypted PEK/BDK/ECDH translation is still missing. | +| `VerifyPinData` | `Partial` | Clear PIN-block verification wrapper exists, but PVV / IBM3624 verification behavior is still missing. | | `GenerateMacEmvPinChange` | `Not yet implemented` | Requires issuer-script PIN-change building blocks. | | `GenerateAs2805KekValidation` | `Not yet implemented` | Requires AS2805-specific KEK-validation primitives. | @@ -53,7 +53,16 @@ Notes: - AWS documents `EncryptData` as supporting symmetric `TDES` and `AES`, asymmetric `RSA`, and derived `DUKPT` or `EMV` schemes. - This starter directly covers only the non-derived AES, TDES, and RSA cases. -## 2) AWS `DecryptData`: AES / TDES / RSA +## 2) AWS `EncryptData`: Payment Wrapper +Operations: +- `Encrypt payment data` + +Suggested use: +- Paste plaintext into the input field as hex. +- Choose a payment-facing profile for AES, TDES, or the implemented DUKPT-TDES wrapper modes. +- Provide the direct key or BDK plus KSN, and add the IV when required. + +## 3) AWS `DecryptData`: AES / TDES / RSA Operations: - `AES Decrypt` or `Triple DES Decrypt` or `RSA Decrypt` @@ -63,7 +72,16 @@ Suggested use: - Paste the key into the key argument using the correct format selector. - Match the AWS algorithm and mode manually in the chosen CyberChef operation. -## 3) AWS `ReEncryptData`: Symmetric Rewrap +## 4) AWS `DecryptData`: Payment Wrapper +Operations: +- `Decrypt payment data` + +Suggested use: +- Paste ciphertext into the input field as hex. +- Choose a payment-facing profile for AES, TDES, or the implemented DUKPT-TDES wrapper modes. +- Provide the direct key or BDK plus KSN, and add the IV when required. + +## 5) AWS `ReEncryptData`: Symmetric Rewrap Operations: - `AES Decrypt` or `Triple DES Decrypt` - `AES Encrypt` or `Triple DES Encrypt` @@ -77,7 +95,16 @@ Notes: - This covers the software-visible decrypt-then-encrypt pattern. - It does not model AWS wrapped-key handling or HSM-side key custody. -## 4) AWS `GenerateMac`: HMAC +## 6) AWS `ReEncryptData`: Payment Wrapper +Operations: +- `Re-encrypt payment data` + +Suggested use: +- Paste source ciphertext into the input field as hex. +- Define the source decrypt profile and the target encrypt profile in one operation. +- Use this as the payment-facing version of the decrypt-then-encrypt recipe chain. + +## 7) AWS `GenerateMac`: HMAC Operations: - `From Hex` - `HMAC` @@ -89,7 +116,7 @@ Suggested use: - Run `HMAC` with the appropriate key and hash function. - If AWS truncates the MAC, use `Take bytes` to keep the leftmost bytes that match `MacLength`. -## 5) AWS `GenerateMac`: CMAC +## 8) AWS `GenerateMac`: CMAC Operations: - `From Hex` - `CMAC` @@ -101,7 +128,7 @@ Suggested use: - Run `CMAC` with `Encryption algorithm` set to `AES` or `Triple DES`. - Use `Take bytes` to match the requested `MacLength` if truncation is required. -## 6) AWS `VerifyMac`: Recompute And Compare +## 9) AWS `VerifyMac`: Recompute And Compare Operations: - `Verify payment MAC` @@ -113,7 +140,7 @@ Notes: - This covers the implemented static-key HMAC/CMAC and DUKPT-CMAC wrapper modes directly. - ISO 9797, EMV MAC, and AS2805-specific verification are still partial gaps. -## 7) AWS `GenerateMac`: Payment Wrapper +## 10) AWS `GenerateMac`: Payment Wrapper Operations: - `Generate payment MAC` @@ -125,7 +152,7 @@ Notes: - This wrapper exists for usability so payment users can stay in the `Payments` category without needing to know which low-level primitive is underneath. - It intentionally reuses the existing generic `HMAC` and `CMAC` implementations. -## 8) AWS `GenerateCardValidationData`: CVV / CVV2 / iCVV +## 11) AWS `GenerateCardValidationData`: CVV / CVV2 / iCVV Operations: - `Generate card validation data` @@ -138,7 +165,7 @@ Notes: - This directly covers software generation of CVV/CVV2/iCVV-style values. - Assumption: CVV2 forces service code `000` and iCVV forces `999`. -## 9) AWS `VerifyCardValidationData`: CVV / CVV2 / iCVV +## 12) AWS `VerifyCardValidationData`: CVV / CVV2 / iCVV Operations: - `Verify card validation data` @@ -152,7 +179,7 @@ Notes: ## Partial Recipe Starters -## 10) AWS `EncryptData` / `DecryptData`: DUKPT-Derived Symmetric Flows +## 13) AWS `EncryptData` / `DecryptData`: DUKPT-Derived Symmetric Flows Operations: - `Derive DUKPT key` - `AES Encrypt` or `AES Decrypt` or `Triple DES Encrypt` or `Triple DES Decrypt` @@ -165,20 +192,20 @@ Notes: - This is useful for offline vector work. - It does not claim one-to-one parity with every AWS DUKPT encryption attribute combination. -## 11) AWS `VerifyAuthRequestCryptogram`: EMV ARQC Check +## 14) AWS `VerifyAuthRequestCryptogram`: EMV ARQC Check Operations: -- `Generate EMV ARQC` +- `Verify EMV ARQC` Suggested use: - Paste the already-assembled EMV authorization-request preimage into the input field as hex. - Provide the already-derived AES session key and cryptogram length. -- Compare the result to the incoming ARQC. +- Provide the incoming ARQC and let the wrapper recompute and compare it. Notes: - This is only practical when the session key and exact preimage assembly are already known. - It is a good fit for AES-CMAC-based profiles, not a full generic EMV verifier. -## 12) AWS `TranslateKeyMaterial`: ECDH And Wrapped-Key Inspection +## 15) AWS `TranslateKeyMaterial`: ECDH And Wrapped-Key Inspection Operations: - `Derive ECDH key material` - `Parse TR-31 key block` @@ -192,7 +219,7 @@ Notes: - This helps with interoperability debugging. - It does not recreate AWS’s HSM-side translate-and-rewrap behavior. -## 13) AWS `GenerateMac`: EMV MAC Preimage Review +## 16) AWS `GenerateMac`: EMV MAC Preimage Review Operations: - `From Hex` - `CMAC` @@ -205,9 +232,9 @@ Notes: - AWS documents `GenerateMac` as supporting EMV MAC. - This fork does not yet have a dedicated EMV MAC operation, so this remains a profile-specific starter rather than a generic implementation. -## 14) AWS `GeneratePinData`: Clear PIN Block Build +## 17) AWS `GeneratePinData`: Clear PIN Block Wrapper Operations: -- `Build PIN block` +- `Generate payment PIN data` Suggested use: - Paste the clear PIN into the input field. @@ -218,9 +245,9 @@ Notes: - This is useful for software test harnesses that need deterministic clear PIN-block construction before encryption. - It does not yet implement PVV generation, IBM 3624 offsets, or encrypted AWS response semantics. -## 15) AWS `TranslatePinData`: Clear PIN Block Translation +## 18) AWS `TranslatePinData`: Clear PIN Block Wrapper Operations: -- `Translate PIN block` +- `Translate payment PIN data` Suggested use: - Paste the source clear PIN block into the input field as hex. @@ -231,13 +258,13 @@ Notes: - This is a software emulation helper for test-vector work. - It does not yet emulate encrypted HSM-bound translation between PEK, BDK, or ECDH-derived keys. -## 16) AWS `VerifyPinData`: Clear PIN Block Inspection +## 19) AWS `VerifyPinData`: Clear PIN Block Wrapper Operations: -- `Parse PIN block` +- `Verify payment PIN data` Suggested use: - Paste the clear PIN block into the input field as hex. -- Decode the PIN-block structure and compare the recovered PIN to your expected test data. +- Provide the expected clear PIN and let the wrapper decode and compare it. Notes: - This is only structural verification today. diff --git a/PAYMENT_RECIPES.md b/PAYMENT_RECIPES.md index 8c5ced0f..4ca2ee9a 100644 --- a/PAYMENT_RECIPES.md +++ b/PAYMENT_RECIPES.md @@ -32,7 +32,22 @@ Suggested use: - Derive IPEK from BDK + KSN. - Derive base session key and apply a variant mask (`PIN`, `MAC Request`, `MAC Response`, `Data`). -## 6) PIN Block Build / Parse / Translate +## 6) Payment Data Encrypt / Decrypt / Re-encrypt +Operations: +- `Encrypt payment data` +- `Decrypt payment data` +- `Re-encrypt payment data` + +Suggested use: +- Paste the message or ciphertext into the input field as hex. +- Choose a payment-facing cipher profile for AES, TDES, or DUKPT-derived TDES. +- Provide the direct key or BDK plus KSN, then add the IV when the selected mode requires one. + +Scope note: +- These wrappers currently cover AES CBC/CTR/ECB, TDES CBC/ECB, and DUKPT-derived TDES CBC/ECB. +- They are intended for software test harnesses and intentionally reuse the existing generic cipher implementations underneath. + +## 7) PIN Block Build / Parse / Translate Operations: - `Build PIN block` - `Parse PIN block` @@ -47,7 +62,21 @@ Scope note: - This starter currently covers clear software test blocks for ISO formats 0, 1, and 3. - It does not yet generate PVV, IBM 3624 offsets, or encrypted PEK/BDK translation flows by itself. -## 7) Card Validation Data (CVV / CVV2 / iCVV) +## 8) Payment PIN Data Wrappers +Operations: +- `Generate payment PIN data` +- `Translate payment PIN data` +- `Verify payment PIN data` + +Suggested use: +- Use these wrappers when you want AWS-style PIN-data naming instead of the lower-level PIN-block operations. +- They currently cover clear ISO 9564 formats 0, 1, and 3 by delegating to the existing build, translate, and parse operations. + +Scope note: +- This is still clear-PIN-block coverage only. +- Encrypted PIN data, PVV, and IBM 3624 are still future additions. + +## 9) Card Validation Data (CVV / CVV2 / iCVV) Operations: - `Generate card validation data` - `Verify card validation data` @@ -62,7 +91,7 @@ Scope note: - CVV2 forces service code `000` and iCVV forces `999`. - It does not try to emulate scheme-specific dCVV, token CVV, or issuer-host formatting differences beyond the common decimalization flow. -## 8) Payment MAC Generation And Verification +## 10) Payment MAC Generation And Verification Operations: - `Generate payment MAC` - `Verify payment MAC` @@ -77,9 +106,10 @@ Scope note: - Current DUKPT coverage derives TDES session keys and applies TDES-CMAC for request and response MAC variants. - ISO 9797, EMV session-derivation MAC, and AS2805 are still future additions. -## 9) EMV ARQC Generation (AES-CMAC Profile) +## 11) EMV ARQC Generation And Verification (AES-CMAC Profile) Operations: - `Generate EMV ARQC` +- `Verify EMV ARQC` Suggested use: - Paste the already-assembled ARQC input block into the input field as hex. @@ -90,7 +120,7 @@ Scope note: - This operation is intentionally limited to AES-CMAC-style EMV profiles. - It does not derive EMV session keys or assemble CDOL/tag data for you. -## 10) EMV ARPC Generation (AES-CMAC Response Profile) +## 12) EMV ARPC Generation (AES-CMAC Response Profile) Operations: - `Generate EMV ARPC` @@ -103,7 +133,7 @@ Scope note: - This operation is intentionally limited to AES-CMAC response profiles where the issuer session key and exact preimage are already known. - Legacy 3DES EMV ARQC/ARPC flows are not covered. -## 11) Combined Message Triage +## 13) Combined Message Triage Operations: - `Parse TR-34 B9 envelope` - `Parse ASN.1 hex string` diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index 72356ef5..24379574 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -581,15 +581,22 @@ "Calculate payment KCV", "Derive ECDH key material", "Derive DUKPT key", + "Encrypt payment data", + "Decrypt payment data", + "Re-encrypt payment data", "Generate payment MAC", "Verify payment MAC", "Generate card validation data", "Verify card validation data", "Generate EMV ARQC", "Generate EMV ARPC", + "Verify EMV ARQC", "Build PIN block", "Parse PIN block", - "Translate PIN block" + "Translate PIN block", + "Generate payment PIN data", + "Translate payment PIN data", + "Verify payment PIN data" ] }, { diff --git a/src/core/lib/PaymentDataCipher.mjs b/src/core/lib/PaymentDataCipher.mjs new file mode 100644 index 00000000..d32901bf --- /dev/null +++ b/src/core/lib/PaymentDataCipher.mjs @@ -0,0 +1,209 @@ +/** + * @license Apache-2.0 + */ + +import OperationError from "../errors/OperationError.mjs"; +import AESEncrypt from "../operations/AESEncrypt.mjs"; +import AESDecrypt from "../operations/AESDecrypt.mjs"; +import TripleDESEncrypt from "../operations/TripleDESEncrypt.mjs"; +import TripleDESDecrypt from "../operations/TripleDESDecrypt.mjs"; +import DeriveDUKPTKey from "../operations/DeriveDUKPTKey.mjs"; + +const PAYMENT_CIPHER_PROFILES = [ + "AES CBC", + "AES CTR", + "AES ECB", + "TDES CBC", + "TDES ECB", + "DUKPT TDES CBC", + "DUKPT TDES ECB", +]; + +const DUKPT_DATA_VARIANTS = ["None", "Data"]; + +/** + * Validates hex input. + * + * @param {string} value + * @param {string} name + * @param {boolean} allowEmpty + * @returns {string} + */ +function normalizeHex(value, name, allowEmpty=false) { + const normalized = (value || "").replace(/\s+/g, "").toUpperCase(); + if (!normalized.length && allowEmpty) return ""; + if (!/^[0-9A-F]+$/.test(normalized) || normalized.length % 2 !== 0) { + throw new OperationError(`${name} must be even-length hex.`); + } + return normalized; +} + +/** + * Resolves the working key for the selected cipher profile. + * + * @param {string} profile + * @param {string} keyHex + * @param {string} ksn + * @param {string} dukptVariant + * @returns {{keyHex: string, keyContext: Object}} + */ +function resolveCipherKey(profile, keyHex, ksn, dukptVariant) { + if (!profile.startsWith("DUKPT ")) { + return { + keyHex: normalizeHex(keyHex, "Key"), + keyContext: { keySource: "Direct key input" } + }; + } + + const normalizedKey = normalizeHex(keyHex, "BDK"); + const normalizedKsn = normalizeHex(ksn, "KSN"); + const dukpt = new DeriveDUKPTKey(); + const derivedKey = dukpt.run(normalizedKey, ["Derive Session Key", normalizedKsn, dukptVariant, false]); + + return { + keyHex: derivedKey, + keyContext: { + keySource: "Derived from DUKPT BDK", + ksn: normalizedKsn, + dukptVariant + } + }; +} + +/** + * Encrypts payment data using the selected profile. + * + * @param {string} inputHex + * @param {string} profile + * @param {string} keyHex + * @param {string} ivHex + * @param {string} ksn + * @param {string} dukptVariant + * @returns {Object} + */ +function encryptPaymentData(inputHex, profile, keyHex, ivHex, ksn, dukptVariant) { + const plaintextHex = normalizeHex(inputHex, "Input data"); + const normalizedIv = normalizeHex(ivHex, "IV", true); + const { keyHex: effectiveKeyHex, keyContext } = resolveCipherKey(profile, keyHex, ksn, dukptVariant); + + let ciphertextHex; + if (profile.startsWith("AES ")) { + const aes = new AESEncrypt(); + const mode = profile.substring(4); + ciphertextHex = aes.run(plaintextHex, [ + { string: effectiveKeyHex, option: "Hex" }, + { string: normalizedIv, option: "Hex" }, + mode, + "Hex", + "Hex", + { string: "", option: "Hex" } + ]).toUpperCase(); + } else { + const tdes = new TripleDESEncrypt(); + const mode = profile.endsWith("CBC") ? "CBC" : "ECB"; + ciphertextHex = tdes.run(plaintextHex, [ + { string: effectiveKeyHex, option: "Hex" }, + { string: normalizedIv, option: "Hex" }, + mode, + "Hex", + "Hex" + ]).toUpperCase(); + } + + return { + profile, + plaintextHex, + ciphertextHex, + ivHex: normalizedIv, + ...keyContext + }; +} + +/** + * Decrypts payment data using the selected profile. + * + * @param {string} inputHex + * @param {string} profile + * @param {string} keyHex + * @param {string} ivHex + * @param {string} ksn + * @param {string} dukptVariant + * @returns {Object} + */ +function decryptPaymentData(inputHex, profile, keyHex, ivHex, ksn, dukptVariant) { + const ciphertextHex = normalizeHex(inputHex, "Input data"); + const normalizedIv = normalizeHex(ivHex, "IV", true); + const { keyHex: effectiveKeyHex, keyContext } = resolveCipherKey(profile, keyHex, ksn, dukptVariant); + + let plaintextHex; + if (profile.startsWith("AES ")) { + const aes = new AESDecrypt(); + const mode = profile.substring(4); + plaintextHex = aes.run(ciphertextHex, [ + { string: effectiveKeyHex, option: "Hex" }, + { string: normalizedIv, option: "Hex" }, + mode, + "Hex", + "Hex", + { string: "", option: "Hex" }, + { string: "", option: "Hex" } + ]).toUpperCase(); + } else { + const tdes = new TripleDESDecrypt(); + const mode = profile.endsWith("CBC") ? "CBC" : "ECB"; + plaintextHex = tdes.run(ciphertextHex, [ + { string: effectiveKeyHex, option: "Hex" }, + { string: normalizedIv, option: "Hex" }, + mode, + "Hex", + "Hex" + ]).toUpperCase(); + } + + return { + profile, + ciphertextHex, + plaintextHex, + ivHex: normalizedIv, + ...keyContext + }; +} + +/** + * Re-encrypts payment data by decrypting under one profile and encrypting under another. + * + * @param {string} inputHex + * @param {Object} params + * @returns {Object} + */ +function reEncryptPaymentData(inputHex, params) { + const decrypted = decryptPaymentData( + inputHex, + params.sourceProfile, + params.sourceKeyHex, + params.sourceIvHex, + params.sourceKsn, + params.sourceDukptVariant + ); + const encrypted = encryptPaymentData( + decrypted.plaintextHex, + params.targetProfile, + params.targetKeyHex, + params.targetIvHex, + params.targetKsn, + params.targetDukptVariant + ); + + return { + source: decrypted, + target: encrypted + }; +} + +export { + DUKPT_DATA_VARIANTS, + PAYMENT_CIPHER_PROFILES, + decryptPaymentData, + encryptPaymentData, + reEncryptPaymentData, +}; diff --git a/src/core/operations/DecryptPaymentData.mjs b/src/core/operations/DecryptPaymentData.mjs new file mode 100644 index 00000000..112196a2 --- /dev/null +++ b/src/core/operations/DecryptPaymentData.mjs @@ -0,0 +1,54 @@ +/** + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import { DUKPT_DATA_VARIANTS, PAYMENT_CIPHER_PROFILES, decryptPaymentData } from "../lib/PaymentDataCipher.mjs"; + +/** + * Decrypt payment data operation. + */ +class DecryptPaymentData extends Operation { + /** + * DecryptPaymentData constructor. + */ + constructor() { + super(); + + this.name = "Decrypt payment data"; + this.module = "Payment"; + this.description = "Paste ciphertext into the input field as hex and decrypt it using a payment-facing cipher wrapper.

Input: ciphertext hex.
Arguments: choose the cipher profile, provide a direct key or BDK, add IV where needed, and provide KSN plus DUKPT variant when using a DUKPT profile."; + this.inlineHelp = "Input: ciphertext hex.
Args: choose AES, TDES, or DUKPT-wrapped TDES, then provide key, IV, and optional KSN context."; + this.testDataSamples = [ + { + name: "AES CBC sample", + input: "76D0627DA1D290436E21A4AF7FCA94B7177C1FC94173D442E36EE79D7CA0E461", + args: ["AES CBC", "00112233445566778899AABBCCDDEEFF", "000102030405060708090A0B0C0D0E0F", "", "Data", false] + } + ]; + this.infoURL = "https://docs.aws.amazon.com/payment-cryptography/latest/DataAPIReference/API_DecryptData.html"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { name: "Cipher profile", type: "option", value: PAYMENT_CIPHER_PROFILES, comment: "Select the payment-facing decryption profile. DUKPT profiles derive a session key first, then run TDES decryption." }, + { name: "Key / BDK", type: "string", value: "", comment: "Provide the clear AES/TDES key for static profiles, or the clear BDK for DUKPT profiles." }, + { name: "IV (hex)", type: "string", value: "", comment: "Initialization vector as hex. Leave blank for ECB. Use 16 bytes for AES CBC/CTR and 8 bytes for TDES CBC." }, + { name: "KSN (DUKPT only)", type: "string", value: "", comment: "Required only for DUKPT profiles. Provide the full 10-byte KSN as hex." }, + { name: "DUKPT variant", type: "option", value: DUKPT_DATA_VARIANTS, defaultIndex: 1, comment: "Applies only to DUKPT profiles. Use Data for the current data-key masking behavior in this fork." }, + { name: "Output as JSON", type: "boolean", value: false, comment: "When enabled, returns the effective cipher context and plaintext." }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [profile, keyHex, ivHex, ksn, dukptVariant, outputJson] = args; + const result = decryptPaymentData(input, profile, keyHex, ivHex, ksn, dukptVariant); + return outputJson ? JSON.stringify(result, null, 4) : result.plaintextHex; + } +} + +export default DecryptPaymentData; diff --git a/src/core/operations/EncryptPaymentData.mjs b/src/core/operations/EncryptPaymentData.mjs new file mode 100644 index 00000000..aad4684f --- /dev/null +++ b/src/core/operations/EncryptPaymentData.mjs @@ -0,0 +1,54 @@ +/** + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import { DUKPT_DATA_VARIANTS, PAYMENT_CIPHER_PROFILES, encryptPaymentData } from "../lib/PaymentDataCipher.mjs"; + +/** + * Encrypt payment data operation. + */ +class EncryptPaymentData extends Operation { + /** + * EncryptPaymentData constructor. + */ + constructor() { + super(); + + this.name = "Encrypt payment data"; + this.module = "Payment"; + this.description = "Paste plaintext into the input field as hex and encrypt it using a payment-facing cipher wrapper.

Input: plaintext hex.
Arguments: choose the cipher profile, provide a direct key or BDK, add IV where needed, and provide KSN plus DUKPT variant when using a DUKPT profile."; + this.inlineHelp = "Input: plaintext hex.
Args: choose AES, TDES, or DUKPT-wrapped TDES, then provide key, IV, and optional KSN context."; + this.testDataSamples = [ + { + name: "AES CBC sample", + input: "00112233445566778899AABBCCDDEEFF", + args: ["AES CBC", "00112233445566778899AABBCCDDEEFF", "000102030405060708090A0B0C0D0E0F", "", "Data", false] + } + ]; + this.infoURL = "https://docs.aws.amazon.com/payment-cryptography/latest/DataAPIReference/API_EncryptData.html"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { name: "Cipher profile", type: "option", value: PAYMENT_CIPHER_PROFILES, comment: "Select the payment-facing encryption profile. DUKPT profiles derive a session key first, then run TDES encryption." }, + { name: "Key / BDK", type: "string", value: "", comment: "Provide the clear AES/TDES key for static profiles, or the clear BDK for DUKPT profiles." }, + { name: "IV (hex)", type: "string", value: "", comment: "Initialization vector as hex. Leave blank for ECB. Use 16 bytes for AES CBC/CTR and 8 bytes for TDES CBC." }, + { name: "KSN (DUKPT only)", type: "string", value: "", comment: "Required only for DUKPT profiles. Provide the full 10-byte KSN as hex." }, + { name: "DUKPT variant", type: "option", value: DUKPT_DATA_VARIANTS, defaultIndex: 1, comment: "Applies only to DUKPT profiles. Use Data for the current data-key masking behavior in this fork." }, + { name: "Output as JSON", type: "boolean", value: false, comment: "When enabled, returns the effective cipher context and ciphertext." }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [profile, keyHex, ivHex, ksn, dukptVariant, outputJson] = args; + const result = encryptPaymentData(input, profile, keyHex, ivHex, ksn, dukptVariant); + return outputJson ? JSON.stringify(result, null, 4) : result.ciphertextHex; + } +} + +export default EncryptPaymentData; diff --git a/src/core/operations/GeneratePaymentPINData.mjs b/src/core/operations/GeneratePaymentPINData.mjs new file mode 100644 index 00000000..b1a97104 --- /dev/null +++ b/src/core/operations/GeneratePaymentPINData.mjs @@ -0,0 +1,54 @@ +/** + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import BuildPINBlock from "./BuildPINBlock.mjs"; + +/** + * Generate payment PIN data operation. + */ +class GeneratePaymentPINData extends Operation { + /** + * GeneratePaymentPINData constructor. + */ + constructor() { + super(); + + this.name = "Generate payment PIN data"; + this.module = "Payment"; + this.description = "Paste the clear PIN into the input field and generate clear PIN-block test data using an AWS-style payment wrapper.

Input: clear PIN digits.
Arguments: choose the PIN-block format, provide the PAN when required, and optionally return structured JSON."; + this.inlineHelp = "Input: clear PIN digits.
Args: choose the block format and provide the PAN for PAN-bound formats."; + this.testDataSamples = [ + { + name: "Format 0 sample", + input: "1234", + args: ["ISO Format 0", "5432101234567890", false, false] + } + ]; + this.infoURL = "https://docs.aws.amazon.com/payment-cryptography/latest/DataAPIReference/API_GeneratePinData.html"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { name: "Format", type: "option", value: ["ISO Format 0", "ISO Format 1", "ISO Format 3"], comment: "Clear ISO 9564 format to generate. This wrapper currently supports only formats 0, 1, and 3." }, + { name: "Primary account number", type: "string", value: "", comment: "Required for formats 0 and 3. Enter digits only." }, + { name: "Randomize fill digits", type: "boolean", value: false, comment: "Affects only formats 1 and 3. Leave disabled for repeatable vectors." }, + { name: "Output as JSON", type: "boolean", value: false, comment: "When enabled, returns the clear PIN block plus the source context." }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [format, pan, randomizeFill, outputJson] = args; + const builder = new BuildPINBlock(); + const pinBlockHex = builder.run(input, [format, pan, randomizeFill]); + const result = { format, pan, pinBlockHex }; + return outputJson ? JSON.stringify(result, null, 4) : pinBlockHex; + } +} + +export default GeneratePaymentPINData; diff --git a/src/core/operations/ReEncryptPaymentData.mjs b/src/core/operations/ReEncryptPaymentData.mjs new file mode 100644 index 00000000..8e2a51d3 --- /dev/null +++ b/src/core/operations/ReEncryptPaymentData.mjs @@ -0,0 +1,70 @@ +/** + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import { DUKPT_DATA_VARIANTS, PAYMENT_CIPHER_PROFILES, reEncryptPaymentData } from "../lib/PaymentDataCipher.mjs"; + +/** + * Re-encrypt payment data operation. + */ +class ReEncryptPaymentData extends Operation { + /** + * ReEncryptPaymentData constructor. + */ + constructor() { + super(); + + this.name = "Re-encrypt payment data"; + this.module = "Payment"; + this.description = "Paste ciphertext into the input field as hex, decrypt it under the source key context, then re-encrypt it under the target key context.

Input: source ciphertext hex.
Arguments: choose source and target profiles, provide the corresponding key or BDK material, add IVs, and supply KSN plus DUKPT variant when using DUKPT profiles."; + this.inlineHelp = "Input: source ciphertext hex.
Args: define the source decrypt context, then the target encrypt context."; + this.testDataSamples = [ + { + name: "AES CBC to TDES CBC sample", + input: "76D0627DA1D290436E21A4AF7FCA94B7177C1FC94173D442E36EE79D7CA0E461", + args: ["AES CBC", "00112233445566778899AABBCCDDEEFF", "000102030405060708090A0B0C0D0E0F", "", "Data", "TDES CBC", "0123456789ABCDEFFEDCBA9876543210", "1234567890ABCDEF", "", "Data", false] + } + ]; + this.infoURL = "https://docs.aws.amazon.com/payment-cryptography/latest/DataAPIReference/API_ReEncryptData.html"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { name: "Source profile", type: "option", value: PAYMENT_CIPHER_PROFILES, comment: "How to decrypt the input ciphertext." }, + { name: "Source key / BDK", type: "string", value: "", comment: "Source clear AES/TDES key or DUKPT BDK." }, + { name: "Source IV (hex)", type: "string", value: "", comment: "Source IV as hex. Leave blank for ECB." }, + { name: "Source KSN (DUKPT only)", type: "string", value: "", comment: "Required only for DUKPT source profiles." }, + { name: "Source DUKPT variant", type: "option", value: DUKPT_DATA_VARIANTS, defaultIndex: 1, comment: "Applies only to DUKPT source profiles." }, + { name: "Target profile", type: "option", value: PAYMENT_CIPHER_PROFILES, comment: "How to encrypt the recovered plaintext." }, + { name: "Target key / BDK", type: "string", value: "", comment: "Target clear AES/TDES key or DUKPT BDK." }, + { name: "Target IV (hex)", type: "string", value: "", comment: "Target IV as hex. Leave blank for ECB." }, + { name: "Target KSN (DUKPT only)", type: "string", value: "", comment: "Required only for DUKPT target profiles." }, + { name: "Target DUKPT variant", type: "option", value: DUKPT_DATA_VARIANTS, defaultIndex: 1, comment: "Applies only to DUKPT target profiles." }, + { name: "Output as JSON", type: "boolean", value: false, comment: "When enabled, returns both the source decrypt and target encrypt contexts." }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [sourceProfile, sourceKeyHex, sourceIvHex, sourceKsn, sourceDukptVariant, targetProfile, targetKeyHex, targetIvHex, targetKsn, targetDukptVariant, outputJson] = args; + const result = reEncryptPaymentData(input, { + sourceProfile, + sourceKeyHex, + sourceIvHex, + sourceKsn, + sourceDukptVariant, + targetProfile, + targetKeyHex, + targetIvHex, + targetKsn, + targetDukptVariant, + }); + return outputJson ? JSON.stringify(result, null, 4) : result.target.ciphertextHex; + } +} + +export default ReEncryptPaymentData; diff --git a/src/core/operations/TranslatePaymentPINData.mjs b/src/core/operations/TranslatePaymentPINData.mjs new file mode 100644 index 00000000..574c4c64 --- /dev/null +++ b/src/core/operations/TranslatePaymentPINData.mjs @@ -0,0 +1,52 @@ +/** + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import TranslatePINBlock from "./TranslatePINBlock.mjs"; + +/** + * Translate payment PIN data operation. + */ +class TranslatePaymentPINData extends Operation { + /** + * TranslatePaymentPINData constructor. + */ + constructor() { + super(); + + this.name = "Translate payment PIN data"; + this.module = "Payment"; + this.description = "Paste a clear PIN block into the input field as hex and translate it between supported clear ISO 9564 formats using an AWS-style wrapper.

Input: clear PIN block hex.
Arguments: choose source and target formats, provide PAN values when required, and optionally randomize target filler digits."; + this.inlineHelp = "Input: source clear PIN block hex.
Args: define source and target format plus PAN context."; + this.testDataSamples = [ + { + name: "Format 0 to 1 sample", + input: "041215FEDCBA9876", + args: ["ISO Format 0", "5432101234567890", "ISO Format 1", "", false] + } + ]; + this.infoURL = "https://docs.aws.amazon.com/payment-cryptography/latest/DataAPIReference/API_TranslatePinData.html"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { name: "Source format", type: "option", value: ["ISO Format 0", "ISO Format 1", "ISO Format 3"], comment: "How to decode the input PIN block." }, + { name: "Source PAN", type: "string", value: "", comment: "Required for source formats 0 and 3." }, + { name: "Target format", type: "option", value: ["ISO Format 0", "ISO Format 1", "ISO Format 3"], defaultIndex: 1, comment: "Target clear PIN-block format." }, + { name: "Target PAN", type: "string", value: "", comment: "Required for target formats 0 and 3." }, + { name: "Randomize target fill digits", type: "boolean", value: false, comment: "Affects only target formats 1 and 3." }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const translator = new TranslatePINBlock(); + return translator.run(input, args); + } +} + +export default TranslatePaymentPINData; diff --git a/src/core/operations/VerifyEMVARQC.mjs b/src/core/operations/VerifyEMVARQC.mjs new file mode 100644 index 00000000..798c612c --- /dev/null +++ b/src/core/operations/VerifyEMVARQC.mjs @@ -0,0 +1,56 @@ +/** + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import { generateEmvAesCmacCryptogram } from "../lib/EmvCryptogram.mjs"; + +/** + * Verify EMV ARQC operation. + */ +class VerifyEMVARQC extends Operation { + /** + * VerifyEMVARQC constructor. + */ + constructor() { + super(); + + this.name = "Verify EMV ARQC"; + this.module = "Payment"; + this.description = "Paste the already-assembled EMV authorization-request input into the input field as hex and verify an AES-CMAC-based ARQC.

Input: preassembled ARQC input data as hex.
Arguments: provide the EMV session key, cryptogram length, and expected ARQC hex value.

This operation intentionally covers only AES-CMAC-style EMV profiles where the session key and preimage are already known."; + this.inlineHelp = "Input: preassembled ARQC data as hex.
Args: provide the AES session key and expected ARQC."; + this.testDataSamples = [ + { + name: "AES-CMAC ARQC verification sample", + input: "000102030405060708090A0B0C0D0E0F", + args: ["00112233445566778899AABBCCDDEEFF", 8, "C1F732B52FB20CAA"] + } + ]; + this.infoURL = "https://docs.aws.amazon.com/payment-cryptography/latest/DataAPIReference/API_VerifyAuthRequestCryptogram.html"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { name: "Session key (hex)", type: "string", value: "", comment: "Provide the already-derived EMV session key as hex. This wrapper does not derive EMV session keys." }, + { name: "Cryptogram bytes", type: "number", value: 8, min: 1, max: 16, comment: "Number of leftmost CMAC bytes to compare." }, + { name: "Expected ARQC (hex)", type: "string", value: "", comment: "Expected ARQC value as hex." }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [sessionKeyHex, cryptogramBytes, expectedArqc] = args; + const generated = generateEmvAesCmacCryptogram(input, sessionKeyHex, cryptogramBytes); + const normalizedExpected = (expectedArqc || "").replace(/\s+/g, "").toUpperCase(); + return JSON.stringify({ + ...generated, + expectedArqcHex: normalizedExpected, + valid: generated.cryptogramHex === normalizedExpected + }, null, 4); + } +} + +export default VerifyEMVARQC; diff --git a/src/core/operations/VerifyPaymentPINData.mjs b/src/core/operations/VerifyPaymentPINData.mjs new file mode 100644 index 00000000..3ac09bfd --- /dev/null +++ b/src/core/operations/VerifyPaymentPINData.mjs @@ -0,0 +1,56 @@ +/** + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; +import ParsePINBlock from "./ParsePINBlock.mjs"; + +/** + * Verify payment PIN data operation. + */ +class VerifyPaymentPINData extends Operation { + /** + * VerifyPaymentPINData constructor. + */ + constructor() { + super(); + + this.name = "Verify payment PIN data"; + this.module = "Payment"; + this.description = "Paste a clear PIN block into the input field as hex and verify it against an expected PIN using an AWS-style wrapper.

Input: clear PIN block hex.
Arguments: choose the format, provide the PAN when required, and supply the expected clear PIN."; + this.inlineHelp = "Input: clear PIN block hex.
Args: define the PIN-block format, PAN context, and expected PIN."; + this.testDataSamples = [ + { + name: "Format 0 verification sample", + input: "041215FEDCBA9876", + args: ["ISO Format 0", "5432101234567890", "1234"] + } + ]; + this.infoURL = "https://docs.aws.amazon.com/payment-cryptography/latest/DataAPIReference/API_VerifyPinData.html"; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { name: "Format", type: "option", value: ["ISO Format 0", "ISO Format 1", "ISO Format 3"], comment: "How to decode the input PIN block." }, + { name: "Primary account number", type: "string", value: "", comment: "Required for formats 0 and 3." }, + { name: "Expected PIN", type: "string", value: "", comment: "Clear PIN digits to compare against." }, + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + const [format, pan, expectedPin] = args; + const parser = new ParsePINBlock(); + const parsed = JSON.parse(parser.run(input, [format, pan])); + return JSON.stringify({ + ...parsed, + expectedPin, + valid: parsed.pin === String(expectedPin || "") + }, null, 4); + } +} + +export default VerifyPaymentPINData; diff --git a/tests/operations/tests/Payment.mjs b/tests/operations/tests/Payment.mjs index c5bf0847..6733b2a6 100644 --- a/tests/operations/tests/Payment.mjs +++ b/tests/operations/tests/Payment.mjs @@ -255,6 +255,57 @@ TestRegister.addTests([ } ] }, + { + name: "Verify EMV ARQC: AES-CMAC profile", + input: "000102030405060708090A0B0C0D0E0F", + expectedOutput: JSON.stringify({ + inputHex: "000102030405060708090A0B0C0D0E0F", + outputBytes: 8, + fullMacHex: "C1F732B52FB20CAAB58D5B6C78CBD514", + cryptogramHex: "C1F732B52FB20CAA", + expectedArqcHex: "C1F732B52FB20CAA", + valid: true + }, null, 4), + recipeConfig: [ + { + op: "Verify EMV ARQC", + args: ["00112233445566778899AABBCCDDEEFF", 8, "C1F732B52FB20CAA"] + } + ] + }, + { + name: "Encrypt payment data: AES CBC", + input: "00112233445566778899AABBCCDDEEFF", + expectedOutput: "67423557CA0509243B9EE04A5DA3448AA397F6D29B5C8BCE065D9CDC936B7F9B", + recipeConfig: [ + { + op: "Encrypt payment data", + args: ["AES CBC", "00112233445566778899AABBCCDDEEFF", "000102030405060708090A0B0C0D0E0F", "", "Data", false] + } + ] + }, + { + name: "Decrypt payment data: AES CBC", + input: "67423557CA0509243B9EE04A5DA3448AA397F6D29B5C8BCE065D9CDC936B7F9B", + expectedOutput: "00112233445566778899AABBCCDDEEFF", + recipeConfig: [ + { + op: "Decrypt payment data", + args: ["AES CBC", "00112233445566778899AABBCCDDEEFF", "000102030405060708090A0B0C0D0E0F", "", "Data", false] + } + ] + }, + { + name: "Re-encrypt payment data: AES CBC to TDES CBC", + input: "67423557CA0509243B9EE04A5DA3448AA397F6D29B5C8BCE065D9CDC936B7F9B", + expectedOutput: "C47BC6E91A9D566F649D750BCE1CE9889FB5AE1489A16692", + recipeConfig: [ + { + op: "Re-encrypt payment data", + args: ["AES CBC", "00112233445566778899AABBCCDDEEFF", "000102030405060708090A0B0C0D0E0F", "", "Data", "TDES CBC", "0123456789ABCDEFFEDCBA9876543210", "1234567890ABCDEF", "", "Data", false] + } + ] + }, { name: "Generate payment MAC: AES-CMAC", input: "1122334455667788", @@ -309,6 +360,63 @@ TestRegister.addTests([ } ] }, + { + name: "Generate payment PIN data: ISO Format 0", + input: "1234", + expectedOutput: "041215FEDCBA9876", + recipeConfig: [ + { + op: "Generate payment PIN data", + args: ["ISO Format 0", "5432101234567890", false, false] + } + ] + }, + { + name: "Translate payment PIN data: ISO Format 0 to ISO Format 1", + input: "041215FEDCBA9876", + expectedOutput: JSON.stringify({ + source: { + format: "ISO Format 0", + pin: "1234", + pinLength: 4, + pinFieldHex: "041234FFFFFFFFFF", + panFieldHex: "0000210123456789", + blockHex: "041215FEDCBA9876", + fillDigitsHex: "FFFFFFFFFF" + }, + target: { + format: "ISO Format 1", + blockHex: "141234FFFFFFFFFF" + } + }, null, 4), + recipeConfig: [ + { + op: "Translate payment PIN data", + args: ["ISO Format 0", "5432101234567890", "ISO Format 1", "", false] + } + ] + }, + { + name: "Verify payment PIN data: ISO Format 0", + input: "041215FEDCBA9876", + expectedOutput: JSON.stringify({ + format: "ISO Format 0", + pin: "1234", + pinLength: 4, + pinFieldHex: "041234FFFFFFFFFF", + panFieldHex: "0000210123456789", + blockHex: "041215FEDCBA9876", + fillDigitsHex: "FFFFFFFFFF", + expectedPin: "1234", + valid: true + }, null, 4), + recipeConfig: [ + { + op: "Verify payment PIN data", + args: ["ISO Format 0", "5432101234567890", "1234"] + } + ] + }, { name: "Derive ECDH key material: raw shared secret", input: ecdhPrivateKey,