Documentation
¶
Overview ¶
Package emv decodes EMV BER-TLV structures from contactless and contact payment card APDU responses. Pure offline parser — no hardware, no network — so the same code paths run in unit tests and in any host-side tooling that consumes captured EMV data (saved NFC reads, debugger transcripts, EMV Co specification examples).
Wrap-vs-native judgement: EMV BER-TLV is a well-documented public format (EMV Book 3 §B Annex B). The walker is ~100 lines of bit-twiddling over a byte slice. Wrapping a FAP for this would add an SD-card install step + a firmware-fork dependency for what is, ultimately, a recursive descent parser. We implement natively here so operators can decode an EMV transcript they pasted from a forum post without a Flipper attached.
What this package covers:
- BER-TLV walker with multi-byte tag + length support
- Constructed vs primitive recognition (per the BER class+P/C bit)
- Curated tag-name table for the ~80 most-common EMV tags
What this package does NOT cover (deliberately out of scope):
- Cryptogram verification (Application Cryptogram derivation, CDA, DDA — these need issuer public keys we don't have)
- Online authorisation flow (issuer scripting, ARPC)
- TLV write / re-encode (round-tripping a tree back to bytes — happy to add if a caller materialises)
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Encode ¶ added in v0.382.0
Encode serialises a list of TLVs back into EMV BER-TLV bytes — the inverse of ParseBytes. For each TLV it emits the tag bytes, the definite length (minimal short/long form), and the value. Whether a tag is constructed is taken from its own P/C bit (0x20 of the first tag byte), exactly as Parse reads it: constructed tags are rebuilt from Children, primitive tags from Value. So Encode(Parse(x)) reproduces a minimally-encoded x.
Wrap-vs-native judgement ¶
Native, and the inverse of the existing parser. EMV BER-TLV is a fully public, deterministic structure (ISO/IEC 8825-1 BER + EMV Book 3); encoding is pure tag/length/value byte assembly — no crypto, no hardware. It builds the TLV blobs an operator sends to a card (PDOL/GPO/command data) or stages for a response; generation only, no card I/O. Correctness is verifiable two ways: round-trip against ParseBytes and hand-computed TLV bytes.
Types ¶
type AFL ¶ added in v0.416.0
type AFL struct {
Entries []AFLEntry `json:"entries"`
ReadRecords []ReadRecord `json:"read_records"`
TotalRecords int `json:"total_records"`
}
AFL is a decoded EMV Application File Locator (tag 94), returned by the card in the GET PROCESSING OPTIONS response. It drives which records the terminal reads next.
func DecodeAFL ¶ added in v0.416.0
DecodeAFL decodes the raw bytes of an EMV Application File Locator. The AFL is a sequence of 4-byte entries — [SFI<<3 | 0][first record][last record] [ODA record count] — with no checksum, so correctness is gated structurally: the length must be a non-zero multiple of 4, each SFI must be 1-30, the record range must be ascending, and the ODA count cannot exceed the range. A blob that fails any of these is rejected rather than mis-decoded.
func DecodeAFLHex ¶ added in v0.416.0
DecodeAFLHex is the hex-string convenience wrapper.
type AFLEntry ¶ added in v0.416.0
type AFLEntry struct {
SFI int `json:"sfi"`
FirstRecord int `json:"first_record"`
LastRecord int `json:"last_record"`
ODARecords int `json:"oda_records"`
Records []int `json:"records"`
}
AFLEntry is one 4-byte entry of an EMV Application File Locator: a short file identifier (SFI) and the inclusive record range the terminal must READ RECORD from it, plus how many of those records participate in offline data authentication (ODA).
type CVMList ¶ added in v0.426.0
type CVMList struct {
AmountX uint32 `json:"amount_x"`
AmountY uint32 `json:"amount_y"`
Rules []CVMRule `json:"rules"`
Notes []string `json:"notes,omitempty"`
}
CVMList is a decoded EMV Cardholder Verification Method List (tag 8E): two 4-byte amount fields (X and Y, referenced by the per-rule conditions) and a sequence of 2-byte rules.
func DecodeCVMList ¶ added in v0.426.0
DecodeCVMList decodes the raw bytes of EMV tag 8E (CVM List). The layout is fixed — 4-byte Amount X, 4-byte Amount Y, then 2-byte rules — so it is gated structurally: at least the 8-byte amount header must be present and the remaining bytes must be an even number of rule bytes. Each rule's method and condition bytes are always surfaced raw; the EMV-table name is added as a best-effort label, with codes outside the table flagged rather than guessed.
func DecodeCVMListHex ¶ added in v0.426.0
DecodeCVMListHex is the hex-string convenience wrapper.
type CVMRule ¶ added in v0.426.0
type CVMRule struct {
Raw string `json:"raw"` // the 2 bytes, hex
MethodByte string `json:"method_byte"`
MethodCode int `json:"method_code"` // low 6 bits
Method string `json:"method"`
ApplyNextIfUnsuccessful bool `json:"apply_next_if_unsuccessful"` // bit 7 (0x40): else fail CVM
ConditionByte string `json:"condition_byte"`
Condition string `json:"condition"`
}
CVMRule is one Cardholder Verification Method rule from a CVM List (tag 8E): a method byte + a condition byte.
type DOL ¶ added in v0.415.0
type DOL struct {
Entries []DOLEntry `json:"entries"`
Count int `json:"count"`
TotalLength int `json:"total_length"`
}
DOL is a decoded EMV Data Object List — PDOL (tag 9F38), CDOL1/CDOL2 (8C / 8D), DDOL (9F49), or TDOL (97). TotalLength is the size of the concatenated value field the terminal must build and hand back (e.g. the GPO command data assembled from a PDOL).
func DecodeDOL ¶ added in v0.415.0
DecodeDOL decodes the raw bytes of an EMV Data Object List: a concatenation of (BER tag, BER length) pairs with NO value bytes between them. This is why the BER-TLV walker can't parse a DOL — there are no values to walk — and why tag 9F38/8C/8D's value is left raw. Tag names are resolved from the same curated table the TLV walker uses; the parse is purely structural (tag + length header bytes) so there is nothing to mis-decode.
func DecodeDOLHex ¶ added in v0.415.0
DecodeDOLHex is the hex-string convenience wrapper.
type DOLEntry ¶ added in v0.415.0
type DOLEntry struct {
Tag uint32 `json:"tag"`
TagHex string `json:"tag_hex"`
Name string `json:"name,omitempty"`
Length int `json:"length"`
}
DOLEntry is one (tag, length) request inside an EMV Data Object List. A DOL carries no values — only the tags the card asks the terminal to supply and how many bytes each must occupy.
type Magstripe ¶ added in v0.453.0
type Magstripe struct {
Track1 *Track1 `json:"track1,omitempty"`
Track2 *Track2 `json:"track2,omitempty"`
Notes []string `json:"notes,omitempty"`
}
Magstripe is the parsed contents of a raw magnetic-stripe swipe — the ASCII track data a card reader / MSR / skimmer emits, as opposed to the EMV chip's tag-57 BCD Track-2-Equivalent (see DecodeTrack2). It carries Track 1 and/or Track 2 as present in the input.
func DecodeMagstripe ¶ added in v0.453.0
DecodeMagstripe parses a raw swipe string containing Track 1 (starting '%', ending '?') and/or Track 2 (starting ';', ending '?'), in any order. The trailing LRC character (after '?') is surfaced raw but not validated — its check is on the bit-level 5/7-bit encoding, a layer below the ASCII string a reader emits, and a wrong verdict is worse than none.
type ReadRecord ¶ added in v0.416.0
ReadRecord is one implied READ RECORD command (SFI + record number) the terminal issues to walk the AFL.
type TLV ¶
type TLV struct {
// Tag is the BER tag as a uint32 — the encoded big-endian bytes
// of the tag identifier. Single-byte tags use the low byte;
// multi-byte tags pack into successively higher bytes (so 0x9F02
// is the most common Amount Authorised tag, 0x5F2A is the
// Transaction Currency Code, etc.). Stored as uint32 so the
// tag-name lookup map can key on it without a string conversion.
Tag uint32 `json:"tag"`
// TagHex is the operator-facing rendering of Tag — always
// uppercase, no 0x prefix, no leading zeros. Matches the format
// every EMV book / forum post uses ("9F02", "5F2A").
TagHex string `json:"tag_hex"`
// Name is the canonical EMV name for the tag, or "" when the
// tag isn't in the curated lookup table.
Name string `json:"name,omitempty"`
// Constructed reports whether the value bytes contain nested
// TLVs (per BER class+P/C bit). When true, Children holds the
// parsed sub-tree and Value is the raw bytes (kept for callers
// that want to re-emit the original structure).
Constructed bool `json:"constructed"`
Value []byte `json:"value,omitempty"`
// ValueHex is the operator-facing hex rendering of Value.
// Convenient for JSON output without forcing every caller to
// re-encode.
ValueHex string `json:"value_hex,omitempty"`
// Children is non-nil iff Constructed is true. May be empty
// when a constructed tag's body is zero-length (legal per the
// spec, occasionally seen in templated responses).
Children []TLV `json:"children,omitempty"`
}
TLV is one decoded BER-TLV entry. Constructed entries carry their child TLVs in Children; primitive entries carry the raw value bytes in Value (Children is nil).
func Parse ¶
Parse decodes a hex-encoded EMV BER-TLV blob (the common form operator-supplied EMV captures take) into a flat list of top-level TLVs. Constructed tags are walked recursively into each TLV's Children. Returns an error on malformed input (truncated tag/length, length-exceeds-buffer, length-encoding reserved value).
func ParseBytes ¶
ParseBytes is the byte-slice variant of Parse for callers that already have raw EMV bytes (e.g. from a PC/SC reader). Same recursive walker; same error contract.
type Track1 ¶ added in v0.453.0
type Track1 struct {
FormatCode string `json:"format_code"` // 'B' = financial/bank
PAN string `json:"pan"`
PANMasked string `json:"pan_masked"`
Name string `json:"name,omitempty"`
Surname string `json:"surname,omitempty"`
GivenName string `json:"given_name,omitempty"`
Expiry string `json:"expiry,omitempty"` // raw YYMM
ExpiryFormatted string `json:"expiry_mm_yy,omitempty"` // MM/YY
ServiceCode string `json:"service_code,omitempty"`
ServiceCodeMeaning string `json:"service_code_meaning,omitempty"`
Discretionary string `json:"discretionary_data,omitempty"`
LuhnValid bool `json:"luhn_valid"`
LRC string `json:"lrc,omitempty"` // trailing redundancy char, surfaced raw (not validated)
Notes []string `json:"notes,omitempty"`
}
Track1 is the decoded ISO 7813 Track 1 (IATA) format — the only track that carries the cardholder name and a format code.
type Track2 ¶ added in v0.414.0
type Track2 struct {
PAN string `json:"pan"`
PANMasked string `json:"pan_masked"`
Expiry string `json:"expiry"` // raw YYMM as encoded
ExpiryFormatted string `json:"expiry_mm_yy"` // MM/YY
ServiceCode string `json:"service_code"` // 3 digits
ServiceCodeMeaning string `json:"service_code_meaning,omitempty"`
Discretionary string `json:"discretionary_data,omitempty"`
LuhnValid bool `json:"luhn_valid"`
Notes []string `json:"notes,omitempty"`
}
Track2 is the decoded contents of EMV tag 57 (Track 2 Equivalent Data) / ISO 7813 track 2. The BER-TLV walker in this package surfaces tag 57's raw value bytes but leaves the nibble-packed track structure untouched; DecodeTrack2 cracks it into the security-relevant fields.
func DecodeTrack2 ¶ added in v0.414.0
DecodeTrack2 decodes the raw bytes of EMV tag 57 (Track 2 Equivalent Data). The format is nibble-packed BCD: <PAN> 'D' <YYMM expiry> <3-digit service code> <discretionary data> with an optional trailing 'F' pad nibble. The PAN's trailing Luhn check digit is the verification anchor — the decode is reported with luhn_valid so a misframed blob is surfaced, never asserted as a valid card number.
func DecodeTrack2Hex ¶ added in v0.414.0
DecodeTrack2Hex is the hex-string convenience wrapper.