emv

package
v0.511.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 4, 2026 License: AGPL-3.0 Imports: 4 Imported by: 0

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

func Encode(tlvs []TLV) ([]byte, error)

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.

func IsDOLTag added in v0.415.0

func IsDOLTag(tag uint32) bool

IsDOLTag reports whether tag is one of the EMV Data Object List tags (PDOL / CDOL1 / CDOL2 / DDOL / TDOL).

func TagName

func TagName(tag uint32) string

TagName returns the canonical EMV name for a tag, or the empty string when the tag isn't in the curated table. Lookups are case-insensitive on the encoded form because the input is a uint32 (no character-case ambiguity).

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

func DecodeAFL(raw []byte) (*AFL, error)

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

func DecodeAFLHex(s string) (*AFL, error)

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

func DecodeCVMList(raw []byte) (*CVMList, error)

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

func DecodeCVMListHex(s string) (*CVMList, error)

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

func DecodeDOL(raw []byte) (*DOL, error)

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

func DecodeDOLHex(s string) (*DOL, error)

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

func DecodeMagstripe(s string) (*Magstripe, error)

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

type ReadRecord struct {
	SFI    int `json:"sfi"`
	Record int `json:"record"`
}

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

func Parse(hexBlob string) ([]TLV, error)

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

func ParseBytes(b []byte) ([]TLV, error)

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

func DecodeTrack2(raw []byte) (*Track2, error)

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

func DecodeTrack2Hex(s string) (*Track2, error)

DecodeTrack2Hex is the hex-string convenience wrapper.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL