Skip to content

Instantly share code, notes, and snippets.

@dasjestyr
Created January 12, 2016 19:07
Show Gist options
  • Select an option

  • Save dasjestyr/2d707d9e4f685e3b8be7 to your computer and use it in GitHub Desktop.

Select an option

Save dasjestyr/2d707d9e4f685e3b8be7 to your computer and use it in GitHub Desktop.
/// <summary>
/// List of "approved" emv tags
/// </summary>
public enum EmvTag
{
/// <summary>
/// Terminal verification results (TVR)
/// </summary>
TerminalVerificationResults = 0x95,
/// <summary>
/// Issuer application data (IAD)
/// </summary>
IssuerApplicationData = 0x9F10,
/// <summary>
/// Transaction status information (TSI)
/// </summary>
TransactionStatusInformation = 0x9B,
/// <summary>
/// Authorization response code (ARC)
/// </summary>
AuthorizationResponseCode = 0x8A,
/// <summary>
/// Issuer identification number (IIN)
/// </summary>
IssuerIdentificationNumber = 0x42,
/// <summary>
/// Application identifier (ADF Name)
/// </summary>
ApplicationIdentifierAdfName = 0x4F,
/// <summary>
/// The application label
/// </summary>
ApplicationLabel = 0x50,
/// <summary>
/// The track2 equivalent data
/// </summary>
Track2EquivalentData = 0x57,
/// <summary>
/// Application Primary Account (PAN)
/// </summary>
PrimaryAccountNumber = 0x5A,
/// <summary>
/// The cardholder name
/// </summary>
CardholderName = 0x5F20,
/// <summary>
/// The application expiration date
/// </summary>
ApplicationExpirationDate = 0x5F24,
/// <summary>
/// The application effective date
/// </summary>
ApplicationEffectiveDate = 0x5F25,
/// <summary>
/// The issuer country code
/// </summary>
IssuerCountryCode = 0x5F28,
/// <summary>
/// The service code
/// </summary>
ServiceCode = 0x5F30,
/// <summary>
/// The pan sequence number
/// </summary>
PANSequenceNumber = 0x5F34,
/// <summary>
/// The application interchange profile
/// </summary>
ApplicationInterchangeProfile = 0x82,
/// <summary>
/// The dedicated file
/// </summary>
DedicatedFile = 0x84,
/// <summary>
/// The transaction type
/// </summary>
TransactionType = 0x9C,
/// <summary>
/// The authorized amount
/// </summary>
AuthorizedAmount = 0x9F02,
/// <summary>
/// The other amount numeric
/// </summary>
OtherAmountNumeric = 0x9F03,
/// <summary>
/// Application ID (AID: terminal)
/// </summary>
ApplicationIDTerminal = 0x9F06,
/// <summary>
/// The application version number
/// </summary>
ApplicationVersionNumber = 0x9F09,
/// <summary>
/// The issuer code table index
/// </summary>
IssuerCodeTableIndex = 0x9F11,
/// <summary>
/// The application preferred name
/// </summary>
ApplicationPreferredName = 0x9F12,
/// <summary>
/// The terminal country code
/// </summary>
TerminalCountryCode = 0x9F1A,
/// <summary>
/// The terminal identification
/// </summary>
TerminalIdentification = 0x9F1C,
/// <summary>
/// The interface device serial number (IFD)
/// </summary>
InterfaceDeviceSerialNumber = 0x9F1E,
/// <summary>
/// Application cryptogram (AC)
/// </summary>
ApplicationCryptogram = 0x9F26,
/// <summary>
/// Cryptogram information data (CID)
/// </summary>
CryptogramInformationData = 0x9F27,
/// <summary>
/// The terminal capabilities
/// </summary>
TerminalCapabilities = 0x9F33,
/// <summary>
/// Cardholder verification method results
/// </summary>
CVMResults = 0x9F34,
/// <summary>
/// The terminal type
/// </summary>
TerminalType = 0x9F35,
/// <summary>
/// Application transaction counter (ATC)
/// </summary>
ApplicationTransactionCounter = 0x9F36,
/// <summary>
/// Unpredictable number (UN)
/// </summary>
UnpredictableNumber = 0x9F37,
/// <summary>
/// The transaction sequence counter
/// </summary>
TransactionSequenceCounter = 0x9F41,
/// <summary>
/// Transaction category code
/// </summary>
TransactionCategoryCode = 0x9F53,
/// <summary>
/// Issuer script result
/// </summary>
IssuerScriptResult = 0x9F5B
}
/// <summary>
/// Helper class wraps a tlv tag dictionary and permits easy access for EMV-specific tags.
/// </summary>
public class EmvTlvTagValues
{
private readonly ReadOnlyDictionary<string, string> _tlvDictionary;
public string this[EmvTag tag]
{
get
{
var tagString = ((int)tag).ToString("X2");
return _tlvDictionary.ContainsKey(tagString) ? _tlvDictionary[tagString] : null;
}
}
public string this[string tag] => _tlvDictionary.ContainsKey(tag) ? _tlvDictionary[tag] : null;
/// <summary>
/// Initializes a new instance of the <see cref="EmvTlvTagValues" /> class.
/// </summary>
/// <param name="tlvString">The TLV string.</param>
public EmvTlvTagValues(string tlvString)
{
_tlvDictionary = TlvParser.ReadTlv(tlvString);
}
}
public class TlvParser
{
private static readonly ILogger Logger = new DefaultLogWriter();
private const string LogTitle = "TLV_PARSER";
private const int ExtendedTagMask = 0x1F;
private const int ContinueExtendedtagMask = 0x80;
private const int ExtendedLengthMask = 0x80;
/// <summary>
/// Reads the TLV.
/// </summary>
/// <param name="tlv">The TLV.</param>
/// <returns></returns>
/// <exception cref="System.ArgumentException">Could not create dictionary for tlv tag-values. Are you sure this tlv string doesn't contain duplicate tags?</exception>
public static ReadOnlyDictionary<string, string>ReadTlv(string tlv)
{
var values = ReadTlvWithDupeTags(tlv);
IDictionary<string, string> result;
try
{
result = values.ToDictionary(value => value.Key, value => value.Value);
}
catch (ArgumentException ex)
{
throw new ArgumentException("Could not create dictionary for tlv tag-values. Are you sure this tlv string doesn't contain duplicate tags?", ex);
}
return new ReadOnlyDictionary<string, string>(result);
}
/// <summary>
/// Reads tlv string and returns collection of tags and values. Use when you know there will be duplicate tags for each entry (e.g. CAPK response from XMLPOSServer)
/// </summary>
/// <param name="tlv">The TLV.</param>
/// <returns></returns>
/// <exception cref="System.ArgumentException">tlv</exception>
public static ReadOnlyCollection<KeyValuePair<string, string>> ReadTlvWithDupeTags(string tlv)
{
var result = new List<KeyValuePair<string, string>>();
// validate
if (string.IsNullOrEmpty(tlv))
{
Logger.Debug("TLV was null or empty. Nothing to parse!", LogTitle);
return new ReadOnlyCollection<KeyValuePair<string, string>>(result);
}
if ((tlv.Length & 0x01) != 0x00)
{
const string error = "Invalid TLV size. String must be an even number of bytes!";
Logger.Error(error, LogTitle);
throw new ArgumentException(error, nameof(tlv));
}
// process via state machine
var currentState = TlvParseState.ReadingTag;
var valueLength = 0;
var extendedLengthCount = 0;
var tag = string.Empty;
var value = string.Empty;
for (var i = 0; i < tlv.Length; i += 2)
{
var currentByteHex = tlv.Substring(i, 2);
int currentByte;
// ensure the current byte is a valid hex value
try
{
currentByte = int.Parse(currentByteHex, NumberStyles.HexNumber);
}
catch
{
var error =
$"Invalid TLV. All characters must be hex, but we encountered a non hex value ({currentByteHex})";
Logger.Error(error, LogTitle);
throw new ArgumentException(error, nameof(tlv));
}
// process
switch (currentState)
{
case TlvParseState.ReadingTag:
tag = currentByteHex;
value = string.Empty;
currentState = (currentByte & ExtendedTagMask) == ExtendedTagMask ? TlvParseState.ReadingExtendedTag : TlvParseState.ReadingValueLength;
break;
case TlvParseState.ReadingExtendedTag:
tag = tag + currentByteHex;
if ((currentByte & ContinueExtendedtagMask) == 0x00)
currentState = TlvParseState.ReadingValueLength;
break;
case TlvParseState.ReadingValueLength:
if ((currentByte & ExtendedLengthMask) == ExtendedLengthMask)
{
extendedLengthCount = currentByte ^ ExtendedLengthMask;
currentState = TlvParseState.ReadingExtendedValueLength;
if (extendedLengthCount < 1 || extendedLengthCount > 4)
{
var error =
$"Invalid TLV extended length size (allowable range 1-4 bytes): {extendedLengthCount}";
Logger.Error(error, LogTitle);
throw new ArgumentException(error, nameof(tlv));
}
}
else
{
valueLength = currentByte;
if (valueLength > 0)
{
currentState = TlvParseState.ReadingValue;
}
else
{
result.Add(new KeyValuePair<string, string>(tag.ToUpper(), value));
currentState = TlvParseState.ReadingTag;
}
}
break;
case TlvParseState.ReadingExtendedValueLength:
valueLength = (valueLength << 8) | currentByte;
extendedLengthCount--;
if (extendedLengthCount == 0)
{
currentState = TlvParseState.ReadingValue;
}
break;
case TlvParseState.ReadingValue:
value = value + currentByteHex;
valueLength--;
if (valueLength == 0)
{
result.Add(new KeyValuePair<string, string>(tag.ToUpper(), value));
currentState = TlvParseState.ReadingTag;
}
break;
}
}
return new ReadOnlyCollection<KeyValuePair<string, string>>(result);
}
/// <summary>
/// Writes a TLV string.
/// </summary>
/// <param name="values">The value dictionary where the value is a hex string.</param>
/// <param name="blacklistTags">Return tlv string without certain tags.</param>
/// <returns></returns>
public static string WriteTlv(IDictionary<string, string> values, IEnumerable<string> blacklistTags = null)
{
var tlv = new StringBuilder();
var bl = blacklistTags?.ToList();
foreach (var item in values)
{
var tag = item.Key;
var value = item.Value;
// if the current tag is in the black list, skip it
if (blacklistTags != null && bl.Contains(tag)) continue;
/* TLV = [Tag]-[Length]-[Value]
* Tag is normally 2 bytes, but could be 3 (e.g. muira)
* Length is variable in length (i.e. could be 1 byte, 2 bytes, 3 bytes, etc)
* Value is variable in length. The byte length of the value is defined in the [Length] block of the TLV
*
* NOTE: '8X' is a flag in the [Length] block. If this is the first bit in this block, it denotes that the byte length
* of the [Length] block has been extended beyond the normal 1 byte. Where 8 is the flag denoting the extension, X is
* the byte length of the extended [Length] block. For example, if the first byte of the [Length] block is
* '82', this means that the [Length] block is extended and the new size of the [Length] block is 2 bytes.
*/
// figure out the length of the [length] block
string extendedLength;
var byteLength = value.Length/2;
if (byteLength <= 0x7F)
{
extendedLength = string.Empty;
}
else if (byteLength <= 0xFF)
{
// extended and is 1 byte
extendedLength = "81";
}
else if (byteLength <= 0xFFFF)
{
// extended and is 2 bytes
extendedLength = "82";
}
else if (byteLength <= 0xFFFFFF)
{
// extended and is 3 bytes
extendedLength = "83";
}
else
{
// extended and is 4 bytes (huge)
extendedLength = "84";
}
// pad the hex value if wrong length (2)
var lengthHexValue = byteLength.ToString("X2");
if (lengthHexValue.Length%2 != 0)
{
lengthHexValue = "0" + lengthHexValue;
}
tlv.AppendFormat("{0}{1}{2}{3}", tag, extendedLength, lengthHexValue, value);
}
return tlv.ToString();
}
private enum TlvParseState
{
ReadingTag,
ReadingExtendedTag,
ReadingValueLength,
ReadingExtendedValueLength,
ReadingValue
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment