BER-TLV クラスを Kotlin で書いてみる
仕様のおさらい
T (タグ) フィールドは 3 バイトまで
ITU-T X.690 や ISO/IEC 8825-1 の規定では T/L/V 各フィールドの長さに関する自由度が高く、著しく長いタグやデータ長を表現することも可能です。ISO/IEC 7816 の現在の運用ではタグの長さを 3 バイトまでとしているようなので、今回はそれに従います。簡潔に書くと、こんなルールになっています。
- バイト 1 の下位 5 ビットの何れかが落ちていれば 1 バイトタグ
- バイト 2 の最上位ビットが落ちていれば 2 バイトタグ
- バイト 3 の最上位ビットが落ちていれば 3 バイトタグ
L (レングス) フィールドは 4 バイトまで
L フィールドの長さは、ETSI TS 101 220 の 7.1.2 Length encoding の規定に従って 4 バイトまでとします。バイト 1 の最上位が立っているときの残りビットで後続のバイト長を表現しますが、それを 3 まで ('83') 許容することになります。Definite フォームのみをサポートし、Indefinite フォームは考慮しません。
Length | Byte 1 | Byte 2:4 |
---|---|---|
0 to 127 | Length ('00' to '7F') | N/A |
128 to 255 | '81' | Length ('80' to 'FF') |
256 to 65535 | '82' | Length ('01 00' to 'FF FF') |
65536 to 16777215 | '83' | Length ('01 00 00' to 'FF FF FF') |
V (バリュー) フィールドは 2 種類
データの本体が入るこのフィールドには、Primitive なデータの他、複数の BER-TLV 形式のデータを入れることができます。タグ 1 バイト目の上から 3 ビット目がセットされていれば、BER-TLV 形式のデータが入っていることがわかります。BER-TLV 形式のデータの中に更に BER-TLV 形式のデータが入り、更にその中に .. と入れ子構造を続けることが可能です。
Bit | Data Type |
---|---|
0 | Primitive |
1 | Constructed |
実際に作ってみる
絵にする
Kotlin のコードは、UML ではどのように表現するのが正しいんでしょう。実際のコードには getTag() や getValue() といったゲッター等は登場しないのですが、それらを登場させて Java で書くとするとこんな感じになりますでしょうか。
いずれ COMPREHENSION-TLV 等の別の TLV 形式も取り扱うかもしれないですし、Tlv クラスと BerTlv クラスを分けています。どちらもコンストラクタは隠していて、インスタンスの生成には Static で用意した BerTlv.listFrom() を使います。バイト配列を放り込めば、BerTlv クラスのインスタンスが数珠つなぎになった状態で生成される算段です。isConstructed である場合には、Value を TLV 配列の形式でも提供します。
上の絵は、PlantUML で下記のように書いています。
@startuml skinparam classAttributeIconSize 0 package util { class Tlv { - tag: Int - isConstructed: Boolean - primitiveValue: ByteArray - tlvs: List<Tlv> -- <<create>> #Tlv(tag: Int, valueArg: ByteArray) .. + getTag(): Int + getValue(): ByteArray + getTlvs(): List<Tlv> .. {abstract} + isConstructed(): Boolean {abstract} + listFrom(bytes: ByteArray): List<Tlv> {abstract} + toByteArray(): ByteArray } class BerTlv { {static} + listFrom(bytes: ByteArray): List<Tlv> .. <<create>> -BerTlv(tag: Int, valueArg: ByteArray) .. + isConstructed(): Boolean + listFrom(bytes: ByteArray): List<Tlv> + toByteArray(): ByteArray } } class List BerTlv -right-|> Tlv List o-right- " 0..*" Tlv List --* Tlv @enduml
コードにする
実際に書いてみたコードは、こちらのコミットにあります。
テストコードを引用します。このテストコードでは、複数の階層に渡る BER-TLV 構造を今回作成したクラスにデコードさせています。
@Test fun constructed() { /* | T | 21 | (1) A constructed BER-TLV contains a constructed one and a primitive one | L | 0A | | V | T | 22 | (2) A constructed BER-TLV contains a primitive one and a constructed one | | L | 05 | | | V | T | 01 | (3) A primitive BER-TLV | | | L | 01 | | | | V | 01 | | | | T | 21 | (4) A constructed BER-TLV with no value | | | L | 00 | | | | V | -- | | | T | 02 | (5) A primitive BER-TLV | | L | 01 | | | V | 01 | */ val input = hexStringToByteArray("210A22050101012100020101") val tlvs = BerTlv.listFrom(input) assertThat(tlvs.size).isEqualTo(1) // (1) A constructed BER-TLV contains a constructed one and a primitive one assertThat(tlvs[0].tag).isEqualTo(0x21) assertThat(tlvs[0].isConstructed).isTrue() assertThat(tlvs[0].value).isEqualTo(hexStringToByteArray("22050101012100020101")) assertThat(tlvs[0].toByteArray()).isEqualTo( hexStringToByteArray("210A22050101012100020101")) assertThat(tlvs[0].tlvs.size).isEqualTo(2) // (2) A constructed BER-TLV contains a primitive one and a constructed one assertThat(tlvs[0].tlvs[0].tag).isEqualTo(0x22) assertThat(tlvs[0].tlvs[0].isConstructed).isTrue() assertThat(tlvs[0].tlvs[0].value).isEqualTo(hexStringToByteArray("0101012100")) assertThat(tlvs[0].tlvs[0].toByteArray()).isEqualTo( hexStringToByteArray("22050101012100")) assertThat(tlvs[0].tlvs[0].tlvs.size).isEqualTo(2) // (3) A primitive BER-TLV assertThat(tlvs[0].tlvs[0].tlvs[0].tag).isEqualTo(0x01) assertThat(tlvs[0].tlvs[0].tlvs[0].isConstructed).isFalse() assertThat(tlvs[0].tlvs[0].tlvs[0].value).isEqualTo(hexStringToByteArray("01")) assertThat(tlvs[0].tlvs[0].tlvs[0].toByteArray()).isEqualTo( hexStringToByteArray("010101")) // (4) A constructed BER-TLV with no value assertThat(tlvs[0].tlvs[0].tlvs[1].tag).isEqualTo(0x21) assertThat(tlvs[0].tlvs[0].tlvs[1].isConstructed).isTrue() assertThat(tlvs[0].tlvs[0].tlvs[1].value).isEqualTo(byteArrayOf()) assertThat(tlvs[0].tlvs[0].tlvs[1].toByteArray()).isEqualTo( hexStringToByteArray("2100")) // (5) A primitive BER-TLV assertThat(tlvs[0].tlvs[1].tag).isEqualTo(0x02) assertThat(tlvs[0].tlvs[1].isConstructed).isFalse() assertThat(tlvs[0].tlvs[1].value).isEqualTo(hexStringToByteArray("01")) assertThat(tlvs[0].tlvs[1].toByteArray()).isEqualTo( hexStringToByteArray("020101")) }
BER-TLV のエンコードも見据えて数珠つなぎ構造で考えてはみたものの、デコードする機能だけを持たせるに留めています。各要素の内容や長さ、順番等に関する制限はそれぞれの実際の運用によることもあり、それらをこのレベルで全て考慮するのはあまり得策ではないと思うに至りました。既に存在する BER-TLV 要素の中身の編集や削除は容易ですが、新たな BER-TLV 要素の挿入は厄介そうではないですか。もう一段上のレベル、アプリケーション寄りのところにエンコードを可能にするクラス群を(いつか)設けることにします。