技術記事
JavaScriptでTLVを解析する方法
ICカード(チップカード)の優れた点の一つは、そこから出力されるデータがほぼ常にBER-TLVと呼ばれる標準形式で提供されることです。平たく言えば、Basic Encoding Rules(基本符号化規則)のTag-Length-Value(タグ・長さ・値)形式です(これに関する古風ながら有益な記事は こちら).
BER-TLV形式は、以下によって定義されたASN.1(抽象構文記法)符号化方式の一つです。 ITU X.690これは、インターネット黎明期以前に遡る、非常に古い一連の標準規格です。
チップカードでは、カードデータを符号化するためにTLV方式が使用されます。最もシンプルに言えば、Tag-Length-Value方式とは、例えば「5A」というタグがあり、その値が(例として)連続した16進値「41 11 12 34 56 78 9A BC」で表される8オクテットである場合、TLVエンコーディングは5A084111123456789ABCのようになる、ということです。ここで5Aがタグ、08が長さ、4111123456789ABCが値です。
EMVCo(チップカード全体を支えるカード発行会社のコンソーシアム)は、チップカード取引のための多数の標準タグを定義しています。例えば、5Aは常にPAN(プライマリ・アカウント・ナンバー、つまりカード番号)を、9F02は取引のAuthorized Amount(承認金額)を、5F2DはLanguage Preference(言語設定)を、といった具合に符号化します。EMVco定義タグの完全なリスト(およびその意味)は以下で確認できます。 https://www.eftlab.co.uk/index.php/site-map/knowledge-base/145-emv-nfc-tags.
TLVが自身の長さを符号化していることを考えると、TLVデータの解析は簡単なはずですよね?
まあ、はい。ほぼ。ある意味で。
もしすべてのタグが(5Aのような)シンプルな1バイトの識別子を持っていれば、TLVストリームの解析は本当に超簡単でしょう。しかし、識別子が256通りの値しか取れないのであれば、TLV方式はあまり実用的ではありません。
タグ識別子を拡張可能にするため、Basic Encoding Rulesではマルチバイトのタグも認められています。規則によれば、最初のタグバイトの下位5ビットがすべてセットされている場合、さらにタグ識別子バイトが続きます。後続のバイトでは、さらにバイトが続く場合は最上位ビットがセットされ、最後のバイトでは最上位ビットがゼロになります。例えば、5F24は正当な2バイトのタグ識別子であり、DFEF01は正当な3バイトのタグ、といった具合です。
EMVCo(EMV仕様書の第3巻付録BでBER-TLVを参照により取り込んでいる)は、TLV間の階層的な親子関係(つまり入れ子構造)を可能にするための「ラッパー」タグの概念も認めています。EMVの規則では、タグの最初のバイトの6番目のビットがセットされている場合、そのタグは「constructed(構造化)」であると言われます(私は次の用語を好みます。 複合)。したがって、3バイトのタグFFEE01は、(架空の)TLVである3F0188および3F025544を次のようにラップするのに使用できます:FFEE01073F01883F025544。親タグFFEE01は7バイトのデータを持ち、3バイトのTLVと4バイトのTLVで構成されています。この方式を用いることで、タグのグループは任意の深さまで入れ子にできます。
注意すべき点として、TLVのLengthバイトもマルチバイトになり得ます。ここでの拡張性の規則(EMV第3巻付録B2より引用)は以下のとおりです:
最上位ビットがセットされているLengthバイトは、下位7ビットを「Lengthの長さ」として扱う必要があることを意味します。言い換えれば、Lengthバイトが0x82の場合、(後続の2バイトに)Length情報が2バイト分あることを意味します。5F0F8103AABBCCで表される(架空の)TLVの場合、タグは5F0F、Lengthの長さは1バイト、実際のLengthは3バイト、Valueは AABBCC です。
ドロのように明快ですよね?
以上を踏まえれば、約75行のJavaScriptで完全に汎用的な再帰下降型TLVパーサーを次のように作成できます。
// 'data' は「95050010203000…」などのような形式である必要があります。
// 言い換えると、シリアライズされたTLVを1つの大きな文字列にしたものです。
// TLVオブジェクトが返されます。これを使ってタグ名から値を参照してください。
// TLV['95'] にはタグ95の値が含まれます。
// TLV['9F26'] にはタグ9F26の値が含まれます、など。
ここで採用する手法は、非常にシンプルなものです:
まず、既知のEMVCo(業界標準)タグおよび既知のID TECH独自タグをすべて含む、タグ識別子の大きな辞書を用意します。この辞書を以下のように呼びます。 _KnownTagsそして、次の式が true を返すかどうかで、'5A' のような識別子の存在を確認できます。 _KnownTags[ '5A' ] trueを返します。
次は解析です!
パースアルゴリズムは非常にシンプルです:
一度に2ニブルずつ次の変数に読み込みます。 タグ 変数に読み込み、そのタグが辞書に存在するかどうかをテストします。辞書内のすべてのタグは1、2、または3バイト長なので、6ニブル読んでも既知のタグが見つからなければ、コンソールに「タグが期待されましたが見つかりませんでした」というメッセージを出力したうえで、読み取り枠を2ニブル進めて、何事もなかったかのように処理を続行します。ここで厳格に例外を投げたい場合はそうしても構いませんが、私の哲学としては(もちろん状況にもよりますが)、パーサーはデフォルトでフェイルソフト(フォールトトレラント)であるべきだと考えています。残りの解析済みデータを引き続き利用したい場合に備えてです。
タグが見つかったら、ワーカーメソッド(この場合、readData()という内部関数)を使って、タグを読み飛ばし、Lengthを読み取り、そのLengthを使ってValueを読み取ります。(ここでは、想定されるLengthの最上位ビットを慎重にチェックし、前述のLengthの長さに関する拡張性ハック規則に従う必要があるかどうかを確認しなければなりません。)
そのValueを、ルックアップキーとしてのタグ名のもとで、保存オブジェクトに格納します。 タグ.
最後に、保存オブジェクトを返します。
では、実際の例を試してみましょう。たとえば、次のようなものがあるとします。 ID TECH Augusta チップカードリーダーを使用し、キーボードモードで Quick Chip データを取得しているとします。カードをディップした際にデバイスから出力されるデータは、次のような形式になります。
これは、ID TECH 独自タグ DFEE25 で始まる大きな TLV データブロックです。(ID TECH のタグの意味について詳しく知りたい場合は、 ID TECH TLVタグリファレンスガイド を以下からダウンロードできます: https://idtechproducts.atlassian.net/wiki/spaces/KB/overview)ただし、このブロック内のタグのほとんどは業界標準の EMVCo タグです。上記のブロックを文字列として tagblock という JS 変数に代入し、先述のパーサーを読み込んで parseTags( tagblock ) を実行すると、次のようにタグと値を含むオブジェクトが返されます。
これらのタグの中には空のものもあれば、(9F27 のように)値が 00 のものや、暗号化されているものもあります。しかし基本的に、EMV トランザクションを実行するために必要なタグはすべてここに揃っています。
なぜ TLV のパースに JavaScript を使うのか? その本当の理由をお話ししたら、皆さんが今まさに感じているであろうサスペンスを台無しにしてしまうので、ここでは控えめにお伝えするにとどめます。決済アプリ環境での Node.js の活用方法、JavaScript を使ったクレジットカードリーダーとの通信方法、Servlet や AJAX を使ったバックエンドのテストサーバーへのアクセス方法など、これらすべてを近日中にこちらで取り上げます。ぜひこのブログをブックマークして、またお越しください!
