diff --git a/src/core/operations/ParseTR34B9Envelope.mjs b/src/core/operations/ParseTR34B9Envelope.mjs
index 8e3ee1d7..1843b000 100644
--- a/src/core/operations/ParseTR34B9Envelope.mjs
+++ b/src/core/operations/ParseTR34B9Envelope.mjs
@@ -6,58 +6,118 @@
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
-/**
- * Parses an ASN.1 TLV length field at the given offset.
- *
- * @param {Uint8Array} bytes
- * @param {number} offset
- * @returns {{headerLength: number, valueLength: number}}
- */
+// ── TR-34 message type table ──────────────────────────────────────────────────
+
+const TR34_MESSAGE_TYPES = {
+ B0: "GetCredential — KRD requests KDH credentials",
+ B1: "KeyCertificate — KDH distributes its certificate",
+ B2: "GetData — KDH requests data from KRD",
+ B3: "ReceiveCert — KRD acknowledges certificate receipt",
+ B4: "BindBegin — KDH initiates key transport binding",
+ B5: "BindRequest — KRD sends public key / OWHF to KDH",
+ B6: "BindResponse — KDH sends certificate binding confirmation",
+ B8: "KeyToken — encrypted key transport object (CMS EnvelopedData)",
+ B9: "BindResponse — final key delivery; contains CMS EnvelopedData + signature",
+};
+
+const TR34_ERROR_CODES = {
+ "00": "Success",
+ "01": "General failure",
+ "02": "Invalid message format",
+ "03": "Unsupported message type",
+ "04": "Certificate not found",
+ "05": "Invalid signature",
+ "06": "Decryption failure",
+ "07": "Invalid key usage",
+ "08": "KDH not authorized",
+ "09": "KRD not authorized",
+ "10": "Message replay detected",
+ "11": "Key already loaded",
+ "12": "Invalid random number",
+ "FF": "Unspecified error",
+};
+
+// ── ASN.1 helpers ─────────────────────────────────────────────────────────────
+
function parseAsnLength(bytes, offset) {
- if (offset + 2 > bytes.length) {
+ if (offset + 2 > bytes.length)
throw new OperationError("Insufficient ASN.1 data.");
- }
const first = bytes[offset + 1];
- if ((first & 0x80) === 0) {
+ if ((first & 0x80) === 0)
return { headerLength: 2, valueLength: first };
- }
const lengthOfLength = first & 0x7f;
- if (offset + 2 + lengthOfLength > bytes.length) {
+ if (offset + 2 + lengthOfLength > bytes.length)
throw new OperationError("Invalid ASN.1 length field.");
- }
let valueLength = 0;
- for (let i = 0; i < lengthOfLength; i++) {
+ for (let i = 0; i < lengthOfLength; i++)
valueLength = (valueLength << 8) | bytes[offset + 2 + i];
- }
return { headerLength: 2 + lengthOfLength, valueLength };
}
+/** Best-effort parse of the outermost ASN.1 SEQUENCE tag in a byte array. */
+function peekAsnSequence(bytes) {
+ if (!bytes || bytes.length < 2) return null;
+ if (bytes[0] !== 0x30) return null; // not SEQUENCE
+ try {
+ const meta = parseAsnLength(bytes, 0);
+ return {
+ tag: "0x30 (SEQUENCE)",
+ headerBytes: meta.headerLength,
+ valueLength: meta.valueLength,
+ totalExpected: meta.headerLength + meta.valueLength,
+ complete: (meta.headerLength + meta.valueLength) === bytes.length,
+ };
+ } catch (_) {
+ return null;
+ }
+}
+
+function hexStr(bytes) {
+ return Array.from(bytes, b => b.toString(16).padStart(2, "0").toUpperCase()).join("");
+}
+
+// ── Operation ─────────────────────────────────────────────────────────────────
+
/**
- * Parse TR-34 B9 envelope operation
+ * Parse TR-34 key transport message operation.
*/
class ParseTR34B9Envelope extends Operation {
- /**
- * ParseTR34B9Envelope constructor
- */
constructor() {
super();
- this.name = "Parse TR-34 B9 envelope";
+ this.name = "Parse TR-34 key transport";
this.module = "Payment";
- this.description = "Paste the full B9 response frame into the input field as hex.
Input: complete TR-34 B9 response encoded as hex, including the leading length field.
This operation splits the response into header, response code, authentication data, KCV, envelope data, signature length, signature, and any trailing bytes.";
- this.inlineHelp = "Input: full B9 response frame as hex, including the 2-byte length field.
Args: none.";
+ this.description = [
+ "Parses a TR-34 key transport message frame (hex input) and decodes each section.",
+ "
",
+ "Input: Complete TR-34 message frame encoded as hex, including the 2-byte length prefix.",
+ "
",
+ "TR-34 defines a family of messages (B0–B9) for transporting symmetric keys using RSA.",
+ " This operation auto-detects the message type and labels each field accordingly.",
+ " The Envelope Data section is a CMS EnvelopedData (ASN.1 SEQUENCE) —",
+ " the outer tag and length are shown; full CMS parsing is not performed here.",
+ "
",
+ "Key transport flow: KDH (Key Distribution Host) wraps a symmetric key under the",
+ " KRD's RSA public key and signs the result. KRD verifies the signature, then decrypts.",
+ "
",
+ "References: ANS X9.143, ANSI TR-34, PCI PIN v3.1 Req 18-4.",
+ ].join("");
+
+ this.inlineHelp = "Input: full TR-34 message frame as hex, including the 2-byte length prefix.";
+
this.testDataSamples = [
{
name: "Synthetic B9 parser sample",
input: "001730303030423930303100112233300030303034AABBCCDD",
- args: []
- }
+ args: [],
+ },
];
+
this.infoURL = "https://en.wikipedia.org/wiki/Key_block";
this.inputType = "string";
this.outputType = "string";
@@ -70,69 +130,91 @@ class ParseTR34B9Envelope extends Operation {
*/
run(input) {
const hex = (input || "").replace(/\s+/g, "");
- if (!hex.length) {
- throw new OperationError("No input.");
- }
- if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex)) {
+
+ if (!hex.length) throw new OperationError("No input.");
+ if (hex.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(hex))
throw new OperationError("Input must be hex.");
- }
const bytes = new Uint8Array(hex.match(/.{2}/g).map(h => parseInt(h, 16)));
- if (bytes.length < 12) {
- throw new OperationError("Input too short.");
- }
+ if (bytes.length < 12) throw new OperationError("Input too short for a TR-34 frame.");
+ const notes = [];
+
+ // ── Outer frame ─────────────────────────────────────────────────────
const declaredLength = (bytes[0] << 8) | bytes[1];
let offset = 2;
- const header = String.fromCharCode(...bytes.slice(offset, offset + 4));
- offset += 4;
+ const header = String.fromCharCode(...bytes.slice(offset, offset + 4)); offset += 4;
+ const responseType = String.fromCharCode(...bytes.slice(offset, offset + 2)); offset += 2;
+ const errorCode = String.fromCharCode(...bytes.slice(offset, offset + 2)); offset += 2;
- const responseType = String.fromCharCode(...bytes.slice(offset, offset + 2));
- offset += 2;
+ const msgDesc = TR34_MESSAGE_TYPES[responseType] || "Unknown message type";
+ const errDesc = TR34_ERROR_CODES[errorCode] || "Unknown error code";
- const errorCode = String.fromCharCode(...bytes.slice(offset, offset + 2));
- offset += 2;
-
- const authLenMeta = parseAsnLength(bytes, offset);
- const authTotalLen = authLenMeta.headerLength + authLenMeta.valueLength;
- const authData = bytes.slice(offset, offset + authTotalLen);
- offset += authTotalLen;
-
- const kcv = bytes.slice(offset, offset + 3);
- offset += 3;
-
- const envLenMeta = parseAsnLength(bytes, offset);
- const envTotalLen = envLenMeta.headerLength + envLenMeta.valueLength;
- const envelopeData = bytes.slice(offset, offset + envTotalLen);
- offset += envTotalLen;
-
- const signatureLengthAscii = String.fromCharCode(...bytes.slice(offset, offset + 4));
- offset += 4;
- const signatureLength = parseInt(signatureLengthAscii, 10);
- const signature = Number.isFinite(signatureLength) ? bytes.slice(offset, offset + signatureLength) : new Uint8Array();
- if (Number.isFinite(signatureLength)) {
- offset += signatureLength;
+ if (errorCode !== "00") {
+ notes.push(`Non-zero error code: ${errorCode} — ${errDesc}`);
}
+ // ── Authentication data (ASN.1 variable length) ──────────────────────
+ const authLenMeta = parseAsnLength(bytes, offset);
+ const authTotalLen = authLenMeta.headerLength + authLenMeta.valueLength;
+ const authData = bytes.slice(offset, offset + authTotalLen);
+ const authAsn = peekAsnSequence(authData);
+ offset += authTotalLen;
+
+ // ── KCV (3 bytes) ────────────────────────────────────────────────────
+ const kcv = bytes.slice(offset, offset + 3); offset += 3;
+
+ // ── Envelope data (CMS EnvelopedData, ASN.1 variable length) ─────────
+ const envLenMeta = parseAsnLength(bytes, offset);
+ const envTotalLen = envLenMeta.headerLength + envLenMeta.valueLength;
+ const envelopeData = bytes.slice(offset, offset + envTotalLen);
+ const envAsn = peekAsnSequence(envelopeData);
+ offset += envTotalLen;
+
+ // ── Signature length (4-byte ASCII decimal) ──────────────────────────
+ const sigLenAscii = String.fromCharCode(...bytes.slice(offset, offset + 4)); offset += 4;
+ const sigLength = parseInt(sigLenAscii, 10);
+ const signature = Number.isFinite(sigLength) ? bytes.slice(offset, offset + sigLength) : new Uint8Array();
+ if (Number.isFinite(sigLength)) offset += sigLength;
+
+ // ── Build output ─────────────────────────────────────────────────────
const out = {
declaredLength,
actualLengthExcludingLengthField: bytes.length - 2,
header,
- responseType,
+ messageType: responseType,
+ messageDescription: msgDesc,
errorCode,
- authDataHex: Buffer.from(authData).toString("hex").toUpperCase(),
- kcvHex: Buffer.from(kcv).toString("hex").toUpperCase(),
- envelopeDataHex: Buffer.from(envelopeData).toString("hex").toUpperCase(),
- signatureLengthAscii,
- signatureLength: Number.isFinite(signatureLength) ? signatureLength : null,
- signatureHex: Buffer.from(signature).toString("hex").toUpperCase(),
- trailingHex: Buffer.from(bytes.slice(offset)).toString("hex").toUpperCase()
+ errorDescription: errDesc,
+ authData: {
+ hex: hexStr(authData),
+ byteCount: authData.length,
+ asnOuter: authAsn,
+ },
+ kcvHex: hexStr(kcv),
+ envelopeData: {
+ hex: hexStr(envelopeData),
+ byteCount: envelopeData.length,
+ description: "CMS EnvelopedData — wrapped symmetric key (decrypt with KRD private RSA key)",
+ asnOuter: envAsn,
+ },
+ signatureLengthAscii: sigLenAscii,
+ signatureLength: Number.isFinite(sigLength) ? sigLength : null,
+ signatureHex: hexStr(signature),
+ trailingHex: hexStr(bytes.slice(offset)),
+ notes,
};
+ if (out.declaredLength !== bytes.length - 2) {
+ out.notes.push(
+ `Declared length ${out.declaredLength} does not match ` +
+ `actual payload length ${bytes.length - 2}.`
+ );
+ }
+
return JSON.stringify(out, null, 4);
}
-
}
export default ParseTR34B9Envelope;