HàPhan 河

TLV - Tag Length & Value, minimal data format for communicating

TLV is basically a multi-levels data format represented by an array of bytes. There're no wasted data like JSON with brackets or spaces, or XML with duplicated tag names.

The data includes 3 parts Tag, Length of Value, and Value.

tlv

Read this Article first

Tag - type of TLV

Depend on system or device, there's a pre-defined Tags list.
EMV smartcards also provide common Tags list, these tags should also be considering.

https://emvlab.org/emvtags/all/

Tag's data type is unsign integer 32bit UInt32, it follows ISO/IEC 8825 for BerTLV.

If all bits from 0 → 5 of first byte is set, we use it with all next bytes that have 7th bit is set.

Here is pseudo code:

private func getTag(_ stream: InputStream) -> TLV.Tag? {
   guard let first = stream.readByte() else { return nil }
   
   /// check 5 bits of first byte is set
   guard first & 0x1f == 0x1f else { /// = 0001 1111 = 31
       return TLV.Tag(rawValue: UInt32(first))
   }
  
   var result: [UInt8] = []
   /// read until finding a byte that has 7th bit not set
   while let next = stream.readByte() {
       result.append(next)
       if next & 0x80 != 0x80 { /// = 1000 0000 = 128
           break
       }
   }
   result.insert(first, at: 0)
   return TLV.Tag(rawValue: UInt32(result))
}

extension TLV {
    enum Tag: UInt32, CaseIterable {
        case responseData = 0xe1
        case cardStatus = 0x48
        case applicationLabel = 0x50
        ....
    }
}

Length - length of Value

Because length of Value is dynamic so we need to know how many bytes we should read.

Next TLV starts at the end of current TLV's Value.
For Miura Card Readers the Length is read as following cases of first byte:

If the 8th bit of first byte is not set, use it as Length
If the 8th bit of first byte is set, unset it and use new value as length of Length

func getLength(on stream: InputStream) -> TLV.Length {
    let firstByte = stream.readByte()
    guard firstByte & 0x80 == 0x80 else {
        return TLV.Length(firstByte)
    }
    let lenOfLength = max(Int(first & 0x7f), 1)
    let lengthBytes = stream.readBytes(count: lenOfLength)
    return TLV.Length(lengthBytes)
}

extension TLV {
    typealias Length = UInt32
}

public extension UnsignedInteger {
    init(_ bytes: [UInt8]) {
        precondition(bytes.count <= MemoryLayout<Self>.size)
        var value: UInt64 = 0
        for byte in bytes {
            value <<= 8
            value |= UInt64(byte)
        }
        self.init(value)
    }
}

Value

Value has variety types: String, a sub TLV or list of TLVs.
Because Value can contains sub TLVs, so we can consider TLV is Tree or Graph data structure. Some traversal algorithim like Pre-Order, In-Order, Level Order should be implemented to search sub tags.

The properly way to parse TLV is using Recursive or Queue:

let parser = TLVParser()

extension TLV {
	struct Value {
		let tlvs: OrderedSet<TLV>?
		let string: String?

		init(bytes: [UInt8]) {
			let data = Data(bytes)
			let tlvs = parser.parse(data)
			if !tlvs.isEmpty { self.tlvs = tlvs }
			else { self.string = String(data: data, encoding: .utf8) }
		}
	}
}
struct TLVParser {
	func parse(data: Data) -> OrderedSet<TLV> {
        let stream = InputStream(data: data)
    	stream.open()
    	defer { stream.close() }

		var tlvs = OrderedSet<TLV>()
    	while dataStream.hasBytesAvailable {
	 		guard let tag = getTag(on: stream) else { return tlvs }
			let length = getLength(on stream: stream)
    		let value = Value(stream.readBytes(count: length))
			tlvs.append(TLV(tag, length, value))
    	}
        return tlvs
	} 
}

The result TLVs should be in-order and can be searched using its keys.
In summary, TLV is lightweight but not application developer friendly.
Apple also provide the CryptoTokenKit with TLV, BerTLV parser. But seems not work well with Miura devices because of private Tags.
https://developer.apple.com/documentation/cryptotokenkit/tktlvrecord

Happy Coding.

Comments