NFC

2018年04月04日

電波法適合の NFC モジュールで安価な NFC タグを利用する

はてなブックマークに登録

はじめに

当ブログの記事「NFC タグで秘密情報を安全に携行する試み」では MIFARE Ultralight 準拠の NXP 製「NTAG21x」シリーズに目を向けました。そこでは市場で現在主流のこの NFC タグ製品の機能面での奥の深さを Android デバイスを使って試してみましたが、値段も安くすっかり気に入った NTAG21x を自作の装置でも利用したいと考えました。携帯端末の可搬性、据え置き型装置の拡張性、どちらも日常生活での NFC の実用性を支えている土台的な要素ですね。

NFC リーダ/ライタモジュール製品を探す

そんなわけでまずは電子工作向きの NFC リーダ/ライタモジュールを探すことから始めました。ネットを見渡してみると様々なものが出回っており、以下の製品などが目にとまりました。いずれもなかなか良さそうです。

国外のメーカーによるこれらのモジュールは個人でも簡単に購入することができます。一方、国内メーカーの製品情報を探したところでは、いくつもの企業が NFC リーダ/ライタモジュールの製造・販売を行ってはいるもののそのほとんどが法人向けの組み込み用であることを知りました。 その中で、私自身が今回調べた範囲では、2018年3月時点で個人が一般のディストリビュータから入手可能な国内製品はソニー株式会社様による「RC-S620/S」のみでした。もし他にも入手しやすい国内製品があればぜひご教示下さい。

さて、ここで気になるのがこれらの製品と電波法との関係です。

電波法と NFC モジュールとの関係について

ご承知のように、日本国内で「技適マーク」のない無線機を使用すると電波法違反となる可能性があり、外国製の機器を使いたい場合などにはこのことが最初に確認すべきポイントとなります。では、やはり電磁波を発生させる NFC リーダ/ライタは電波法においてどのように扱われているのでしょう。技適証明が必要なのか、それとも周囲への影響が微弱であるため特に規定されていないのか。この点について調べてみました。

まとめ

結論として、電波法には NFC リーダ/ライタに関する明確な規定があり、それに適合しない製品の日本国内での使用は NG であることを知りました。要点を整理してみます。

  • いわゆる技適証明は特定無線設備を対象とするものだが、NFC リーダ/ライタは電波法において特定無線設備にはあたらず「高周波利用設備」に該当し、そこに含まれる「誘導式読み書き通信設備」に分類される
    • つまり NFC リーダ/ライタは技適マークとは関係がない
  • 誘導式読み書き通信設備は、その電界強度が所定の条件を満たしている場合、または、総務大臣による型式指定を受けている場合には個別の許可なしに設置することができる
    • 型式指定の申請者は日本人または日本国内の法人でありかつ製造業者または輸入業者でなければならない
  • 電界強度条件との整合性の確認にも型式指定を受けるにも適正な方法での性能試験が必須

詳細

型式指定表示の実例

手元にあるふたつの製品での型式指定表示と公式サイトからの引用を示します。前掲の RC-S620/S にもしっかり印刷されています。  ※クリックで大きく表示

RC-S620/S について

貴重な選択肢として

今回手元で調べた範囲では、日本の電波法に準じた型式指定表示のある国外メーカーの NFC モジュールは見当たりませんでした、また、「3mの距離において電界強度が500μV/m」などという性能情報を目にすることもありません。 そのため、いまの時点で手近な実験や電子工作を適法に行うためには入手が容易で電波法適合の RC-S620/S は貴重な選択肢ということになります。

個人的には、電波法の必然性と重要性を十分に理解・納得しながらも、一方で、とりわけ IoT 方面での国外メーカーによる国際的にメジャーな製品を公然と試すことのできないケースが発生することをもどかしく思うこともあります。手近な実験や試作のためにわざわざ電波暗室を借りたり渡航したりするわけにもいきませんしね。。 (^^;
さっそくスイッチサイエンス様のサイトから RC-S620/S を購入しました。 本来組み込み用途のこのモジュールの入出力端子は FFC コネクタで一般的な電子工作では何かと取り回しにくいのですが、同社オリジナルのブレークアウト基板と専用ケーブルのセットがあわせて販売されているためとても助かります。

利用者向けの公式リソース

利用者向けに以下のリソースが公式サイトで公開されています。各ダウンロードページに記載された規約・使用許諾契約の内容を十分に確認した上でこれらを利用します。

  • Sony Japan | FeliCa | 法人のお客様 | ダウンロード
    • RC-S620/S製品仕様書<簡易版>
    • RC-S620/Sコマンドリファレンスマニュアル<簡易版>
      規約
             :
      第3条(使用権)

      1. ソニーは、利用者に対して、利用者が対象文書を使用目的のために本ウェブサイトから複製することができる非独占的な権利を無償にて利用者に許諾します。
      2. 利用者は、対象文書の全部または一部を、さらに複製したり、これに対する修正、追加等の改変をすることはできません。また、対象文書およびその複製物を、第三者に頒布したり、ウェブサイトにアップロードするなどして第三者が取得可能な状態にしないものとします。
             :
  • Arduino向けRC-S620/S制御ライブラリの提供
    • 「Arduino向けRC-S620/S制御ライブラリ」ダウンロードページ
      RC-S620/S制御ライブラリ 使用許諾契約
             :
      第1条(総則)
       1.ソニーは、本規約に定める各条項に従い、本ソフトウェアおよび関連資料に関する非独占的かつ譲渡不能な次の権利(但し特許権は除く。以下「使用権」とします)を、お客様に許諾します。
        (1)お客様が、ソフトウェア使用者をして、本ソフトウェアおよび関連資料の全部もしくは一部を使用、複製、複写または改変してアプリケーションソフトウェアを開発する権利。 なお、本規約において、ソフトウェア使用者およびアプリケーションソフトウェアとは、それぞれ、次の意味を有します。
         .愁侫肇ΕД∋藩兌圈本ソフトウェアを入手頂いたお客様1名の方。
         ▲▲廛螢院璽轡腑鵐愁侫肇ΕД◆Д愁法疾夙鸚椰ICカードリーダ/ライター製品「RC-S620/S」を制御するためのソフトウェアプログラム。
        (2)お客様が、本項第1号に基づき開発されたアプリケーションソフトウェアを複製したうえ、ソースコード形式またはバイナリ形式にて、第三者に頒布する権利。
       2.お客様は、前項で定める場合を除き、本ソフトウェアおよび関連資料の全部もしくは一部を使用、複製、複写または改変してはならないものとします。
             :

公式リソースのカバーする範囲

このようにドキュメントに加えサンプルコードが公式に提供されていることは利用者にとってとても嬉しい配慮ですが、これらの参照・使用に際しては以下の事情をあらかじめ理解しておく必要があるでしょう。

  • RC-S620/S はあくまでも組み込み用製品であり本来の顧客は法人である
  • 各仕様書は冒頭のページの注釈のとおり「調査、試作および評価」を目的とする「簡易版」であり、「商用向け」の版は顧客が「正式に RC-S620/S を導入」する場合に提供される
  • 「製品仕様書<簡易版>」 1, 2 項に記載のあるように RC-S620/S は FeliCa に加え ISO/IEC 14443 (Type A, Type B) にも対応しているが、主眼はあくまでも前者である
  • 各仕様書およびサンプルコードは FeliCa まわりの一部の機能に特化した内容である

思いがけない援軍の存在

さて、冒頭で触れたように手元で実現したいのは MIFARE Ultralight NTAG21x へのアクセスです。RC-S620/S が ISO/IEC 14443 Type A(MIFARE)に対応していることは上記の通り仕様書に明記されているものの、公式のリソースではそのための具体的な方法や手順がわかりません。

まずそもそも MIFARE の近接を検知するにはどうすればいいのか。「Arduino向けRC-S620/S制御ライブラリ」の polling() 関数は FeliCa を検知するために「コマンドリファレンスマニュアル<簡易版>」に記載されている「InListPassiveTarget」というコマンドを呼び出しています。
ためしに「InListPassiveTarget ISO/IEC 14443 Type A」のキーワードで情報を検索したところ次の興味深い資料が目にとまりました。

PN532」は NXP による NFC モジュールです。この資料の中の InListPassiveTarget の記述箇所を探すと Page 115 (上図)にそのものずばりのコマンド説明がありました。

「RC-S620/Sコマンドリファレンスマニュアル<簡易版> Version 2.11」 Page 80 の InListPassiveTarget の説明と読み合わせてみると、ターゲットが FeliCa の場合のパラメータ説明は一致しており、さらに「簡易版」には記載されていない ISO/IEC 14443 Type A, B への対応方法を含め 7 ページに渡り詳しい説明が掲載されています。その他のコマンドについても同様で、CommunicateThruEx など RC-S620/S 側のみに存在するいくつかのコマンドはあるものの、全体としてはいわばスーパーセットのマニュアルのように見受けられました。実際にこのマニュアルに添って「Arduino向けRC-S620/S制御ライブラリ」の polling() 関数を以下の要領で書き換えてみると ISO/IEC 14443 Type A の近接を検知することができました。

FeliCa 対応のオリジナルコード

/* InListPassiveTarget */
memcpy(buf, "\xd4\x4a\x01\x01\x00\xff\xff\x00\x00", 9);
buf[5] = (uint8_t)((systemCode >> 8) & 0xff);
buf[6] = (uint8_t)((systemCode >> 0) & 0xff);

ret = rwCommand(buf, 9, response, &responseLen);

ISO/IEC 14443 Type A 用

ret = rwCommand((const uint8_t *)"\xd4\x4a\x01\x00", 4,
                 response, &responseLen);

このように、メーカーの異なる RC-S620/S と PN532 との間にコマンドレベルでの共通性がある理由は NFC フォーラム仕様のどこかにあるのかも知れませんが手元では今のところ該当する情報の特定に至っていません。 あるいは、両社がともに「デバイスやサービス間の新機能と互換性を実現するための規格策定」をミッションのひとつとする NFC フォーラムのコアメンバーであることに関係があるのかも知れません。いずれにせよ PN532 のマニュアルのおかげで先が見えてきました。

NTAG21x ネイティブコマンドの発行について

先般の記事では、NTAG21x のメモリアクセスに必要な次のよっつのネイティブコマンドを挙げました。

  • GET_VERSION
    NTAG 製品のバージョン,ストレージサイズ等を得る
  • READ
    所定のページアドレスから始まる 4 ページ分(16 バイト)のデータを得る
  • WRITE
    所定のページアドレスへ 1 ページ分(4 バイト)のデータを書き込む
  • PWD_AUTH
    パスワードで保護された領域へのアクセス要求に先行してパスワード認証を要求する

これらを RC-S620/S 経由でターゲットへ送出してやれば NTAG21x のメモリを自由に操作できるはずです。

RC-S620/S にはこのようにターゲットとする PICC 固有のコマンドを発行するための CommunicateThruEX コマンドが用意されています。ただし、「コマンドリファレンスマニュアル<簡易版> Version 2.11」 Page 71 の「本コマンドは 212/424 kbps の RF パケットの送受信を行います」の記述のとおりこのコマンドは FeliCa 専用で、通信速度 106 kbps の ISO/IEC 14443 向けには使えません。

一方、「PN532 User Manual Rev.02」にはそれと同様の機能で汎用性のある InDataExchange, InCommunicateThru のふたつのコマンドの情報が掲載されています。同マニュアルの InListPassiveTarget コマンドのページには MaxTg フィールドについて「The PN532 is capable of handling 2 targets maximum at once, so this field should not exceed 0x02.」と記述されており、InDataExchange においては target (Tg) を明示的に指定するのに対し、InCommunicateThruでは「it is assumed that a target has been first activated.」という違いがあります。

  • PN532 User Manual Rev.02 - 7.3.5 InListPassiveTarget
  • PN532 User Manual Rev.02 - 7.3.8 InDataExchange
  • PN532 User Manual Rev.02 - 7.3.9 InCommunicateThru

ふたたび RC-S620/S 側に目を向けると、「コマンドリファレンスマニュアル<簡易版> Version 2.11」 Page 50 の InListPassiveTarget コマンド説明で MaxTg フィールドについては「Target ID を取得する Target の最大数. 01h を指定してください」と記述されています。つまりこの部分の仕様は PN532 と異なる様子であり、この要素の影響を受ける InDataExchange, InCommunicateThru の挙動は、両コマンドの存在有無の確認を含め RC-S620/S 上で実際に試してみる必要がありそうです。

前掲のよっつの NTAG21x ネイティブコマンドの発行を RC-S620/S 経由で試みたところ、InListPassiveTarget によるターゲットの捕捉後に以下の組み合わせが有効であることを手元で確認しました。また、この結果から RC-S620/S においても InDataExchange, InCommunicateThru の両コマンドを利用可能であることが明らかになりました。

  • NTAG: GET_VERSION コマンド (0x60)
    • InCommunicateThru コマンド(0x42)経由での発行のみ有効
      • rwCommand((const uint8_t*)"\xd4\x42\x60", 3, response, &responseLen); の要領
  • NTAG: READ コマンド (0x30)
    • InDataExchange コマンド(0x40)、InCommunicateThru コマンド(0x42)いずれを経由しても有効
      • rwCommand((const uint8_t*)"\xd4\x40\x01\x30[開始ページ番号 1byte]", 5, response, &responseLen); の要領
      • rwCommand((const uint8_t*)"\xd4\x42\x30[開始ページ番号 1byte]", 4, response, &responseLen); の要領
  • NTAG: WRITE コマンド (0xA2)
    • InDataExchange コマンド(0x40)経由での発行のみ有効
      • rwCommand((const uint8_t*)"\xd4\x40\x01\xA2[対象ページ番号 1byte][書き込みデータ 4byte]", 9, response, &responseLen); の要領
  • NTAG: PWD_AUTH コマンド (0x1B)
    • InCommunicateThru コマンド(0x42)経由での発行のみ有効
      • rwCommand((const uint8_t*)"\xd4\x42\x1b[パスワード 4byte]", 7, response, &responseLen); の要領

ライブラリへの加筆とテストプログラムの作成

ここまでの経緯でおおむね材料が揃ったところで、「Arduino向けRC-S620/S制御ライブラリ」へ自分のほしかった機能を追加し、あわせて動作確認用にテストプログラムの作成を行います。

想定した内容

以下の内容を想定しました。NDEF メッセージの解釈や ISO/IEC 14443 Type B の ID 取得など若干の要素を追加しています。さしあたり手元で用途のなかった NTAG21x ネイティブの WRITE, PWD_AUTH コマンドは今回は利用していません。今後必要になったときに実装を追加しようと思います。

  • ライブラリへ追加する関数のイメージ
    • ISO/IEC 14443 Type A の近接検知と ID の取得
    • ISO/IEC 14443 Type B の近接検知と ID の取得
    • NTAG21x 専用: メモリの総ページ数を取得
    • MIFARE Ultralight 用: 所定のメモリページの内容を読み出す
  • テストプログラムのイメージ
    • ターゲットが ISO/IEC 14443 Type A (MIFARE) の場合
      • ID を取得しシリアルへ出力
        • MIFARE Ultralight の場合
          • メモリ内容のダンプをシリアルへ出力
            • NDEF メッセージが記録されている場合
              • NDEF レコード内容を順次取得しシリアルへ出力
    • ターゲットが ISO/IEC 14443 Type B の場合
      • 擬似 ID である PUPI (Pseudo-Unique PICC Identifier) を取得しシリアルへ出力
    • ターゲットが FeliCa (Standard / Lite / Lite-S) の場合
      • IDm を取得しシリアルへ出力

機器の接続

手元ではこれまで Arduino IDE を利用する機会はありましたが Arduino 実機を扱うのは今回が初めてでした。

RC-S620/S と Arduino UNO をシリアル接続して制御を行い、上記のように処理状況を UNO から USB 経由で PC 上のシリアルターミナルへ随時出力することを想定していたのですが、UNO のハードウェアシリアルが一系統のみであることを知り、別立ての USB シリアル変換モジュール(FT231X)と Arduino 標準の SoftwareSerial ライブラリを併用することにしました。

同じ理由で、Arduino IDE から UNO 本体の USB シリアル経由でプログラムを書き込む際には [RC-S620/S 側 TXD] <--> [RX ピン] の結線を外して排他する必要があります。このあたりの事情は Arduino ユーザには当然のことなのでしょうがビギナーにはちょっと新鮮でした。

  

ソースコード

動作の様子

動画:3分19秒

  


(tanabe)
klab_gijutsu2 at 08:38|この記事のURLComments(0)
2017年12月27日

安価な NFC タグで秘密情報を安全に携行する試み

はてなブックマークに登録

はじめに

最近 NFC まわりの調査と実験を行っています。切り口が多く奥の深い技術ですが、NFC タグ製品について調べている過程で現在もっとも広く出回っている NXP Semiconductors 製の「NTAG21x」シリーズ (ISO/IEC 14443 Type A: MIFARE Ultralight に実装されている興味深いアクセス制御機構を知りました。

NFC タグの一般的な「手軽さ」とは裏腹に、扱いを誤るとタグがあっさり使用不能になるリスクと背中合わせであるためか今のところこの機構に言及した記事やアプリケーションはあまり見かけません。しかし、仕様を理解した上で適切に取り回せばこの安価なタグ製品の使途を大きく拡げることができるでしょう。

  • 媒体が小さく軽薄で嵩張らない
  • 非接触型であるため露出の必要がなく忍ばせやすい
  • 緊急時には簡単に破壊・破棄できる

手元ではこういった物理的な特長にも注目し、NTAG21x を秘密情報の格納に利用するアイディアを形にしてみました。データ圧縮と複数タグへの分割格納にも対応しています。今回はこの NTAG21x のアクセス制御機構に関する情報の整理を行い、試作した Android アプリケーションの内容を紹介します。

ちなみに手元では実験用に NTAG213 を計 110 枚、NTAG216 を計 25 枚調達しました。どちらもまず Amazon で少量を購入し様子見を経て後顧の憂いなく使い潰せるようにより安く入手できる Aliexpress で買い足しを行いました。参考に購入元と購入時の価格を掲載します。(個人的には現時点で Aliexpress を利用する際には万一の事故に備えての措置をとっています)

NTAG213(ユーザメモリ容量 144 バイト)

NTAG216(ユーザメモリ容量 888 バイト)

なお、本記事においては NTAG メモリ内容の参照に NXP 純正の次のアプリケーションを使用しています。

1. NTAG21x のアクセス制御機構とネイティブコマンド

データシート

アクセス制御機構の概要と簡単なデモ

  • NTAG21x メモリ上の任意のページ(1 ページ = 4 バイト)以降へのアクセス要求に対し当該タグをパスワードによって保護できる
  • パスワードによる保護は Write 要求のみならず Read 要求に対しても設定可能
  • パスワード認証連続試行回数の上限値を設定することが可能であり、この回数を超過すると当該タグの認証機能は永久に封鎖されパスワード正否にかかわらず認証通過不能となる

    デモ 1: 試作アプリから NTAG213 にデータの書き込みを行い全ページの読み書きをパスワードで保護する (動画 1分39秒)

    デモ 2: 上記デモ 1 でのパスワード設定時に併せて設定した認証試行上限回数を超過する操作を行った結果タグが自爆する様子 (動画 1分2秒)

アクセス制御機構の詳細

  • データシート中の記事
    • "1. General description"
    • "8.5 Memory organization"
    • "8.6 NFC counter function"
    • "8.8 Password verification protection"
  • 要点のまとめ
    • NTAG213 の EEPROM メモリの総ページ数は 45(0h〜2Ch)。NTAG215 は 135 ページ, NTAG216 は 231 ページであり、いずれも末尾の 4 ページが Configuration Pages である。下図の NTAG213 のメモリレイアウトを参照のこと。なお 1 ページ = 4 バイト
    • Configuration 第 1 ページの第 4 バイト「AUTH0」にはパスワード保護対象とする開始ページのアドレスを指定する。このフィールドに実在ページの範囲を超えるアドレスが格納された状態ではパスワード保護は無効。出荷時のデフォルトは 0xFF
    • Configuration 第 2 ページの第 1 バイト「ACCESS」の PROT ビットが 0 ならパスワード保護の対象は Write 要求のみ。同ビットが 1 なら Read, Write 要求の両方が対象となる
    • パスワード(PWD)は 4バイト固定。Configuration 第 3 ページへ格納。出荷時の PWD のデフォルトは 0xFFFFFFFF
      • PWD は可読文字列でなくても許容されるためバリエーションは 4 バイトで 256^4 であり用途によっては意図的に非 ASCII データを適用する選択もあり得るだろう
      • 「ACCESS」の AUTHLIM にはブルートフォース攻撃対策用にパスワード認証要求(PWD_AUTH:後出)試行回数上限を設定可能。認証連続失敗回数がこの値を超えると以降の PWD_AUTH はパスワードの正否にかかわらず永久に無効となる。試行可能回数の範囲で PWD_AUTH が成功すると失敗回数を保持する内部カウンタはリセットされる。AUTHLIM の初期値は当該機能の無効を示す 000b
    • Configuration 第 4 ページの先頭 16 ビットの Password Acknowledge(PACK)はパスワード認証通過時の肯定応答値。デフォルトは 0x0000
    • パスワード保護が有効なメモリ領域の読み書きはパスワード認証を通過し PACK を得た後に可能となる
    • PWD および PACK の読み出しは不可能。両フィールドへの READ 要求に対し NTAG は常に 0x00 を返す
    • NTAG21x のパスワード保護機構は不正なメモリアクセスの防止に特化したものでありデータ暗号化等はアプリケーション側の要件
    • Configuration Pages を永続的にロックする CFGLCK 機構にも要注意
  • フィールドの整理
    (※データシートから引用した図・表を説明のために再構成しています)


  • (参考)NTAG213 初期状態のメモリダンプ:ページ 29h 以降が Configuration Pages
    ※ NXP 提供の Android アプリ TagInfo での表示
     

関連する NTAG コマンド

上記アクセス制御機構を利用する上で最低限必要な NTAG21x ネイティブコマンドおよび関連情報を示す。

  • データシート中の記事
    • "9. Command overview"
  • 要点のまとめ
    • NTAG コマンドに対する最短のレスポンスは 4ビットの ACK or NACK である
      • Ah: Acknowledge (ACK)
      • 0h: NAK for invalid argument (i.e. invalid page address)
      • 1h: NAK for parity or CRC error
      • 4h: NAK for invalid authentication counter overflow
      • 5h: NAK for EEPROM write error
    • コマンドに付与する CRC 値は ISO/IEC 14443 の規約に準拠のこと (実装例

    • GET_VERSION コマンド
      NTAG 製品のバージョン,ストレージサイズ等を得る。Configuration Pages のアドレス取得に必要
      • リクエスト
        • コマンド 1 バイト(60h) + CRC 2 バイト
      • レスポンス
        • OK: データ 8 バイト+ CRC 2 バイト
        • NG: NAK 各値
      NTAG 21x GET_VERSION レスポンスデータの内容
      
      Byte no. Description       NTAG213 NTAG215 NTAG216  Interpretation
            0  fixed Header          00h     00h     00h
            1  vendor ID             04h     04h     04h  NXP Semiconductors
            2  product type          04h     04h     04h  NTAG
            3  product subtype       02h     02h     02h  50 pF
            4  major product version 01h     01h     01h  1
            5  minor product version 00h     00h     00h  V0
            6  storage size          0Fh     11h     13h  see following information
            7  protocol type         03h     03h     03h  ISO/IEC 14443-3 compliant
      

      NTAG 製品の識別方法

      storage size の上位 7ビットを符号なし整数値 n として解釈する。使用可能なユーザーメモリの合計サイズは、最下位ビットが 0 の場合には 2^n であり最下位ビットが 1 の場合には 2^n 〜 2^(n+1) である。これを識別に利用する。

      NTAG213 での GET_VERSION レスポンス実例

      HEX: 00 04 04 02 01 00 0F 03

      --> 0x0F = BIN: 00001111 --> BIN: 0000111 = 0x07 --> 2^7 = 128, 2^(7+1) = 256

      ※ 128〜256 バイトのユーザーメモリ空間を持つ製品は NTAG213(144 バイト)

      NTAG216 での GET_VERSION レスポンス実例

      HEX: 00 04 04 02 01 00 13 03

      --> 0x13 = BIN: 00010011 --> BIN: 0001001 = 0x09 --> 2^9 = 512, 2^(9+1) = 1024

      ※ 512〜1024 バイトのユーザーメモリ空間を持つ製品は NTAG216(888 バイト)

    • READ コマンド
      所定のページアドレスから始まる 4 ページ分(16 バイト)のデータを得る
      • リクエスト
        • コマンド 1 バイト(30h)+ 開始アドレス 1 バイト + CRC 2 バイト
      • レスポンス
        • OK: データ 16 バイト+ CRC 2 バイト
        • NG: NAK 各値
    • WRITE コマンド
      所定のページアドレスへ 1 ページ分(4 バイト)のデータを書き込む
      • リクエスト
        • コマンド 1バイト(A2h)+ 対象アドレス 1 バイト + データ 4 バイト + CRC 2 バイト
      • レスポンス
        • OK: ACK
        • NG: NAK 各値
    • PWD_AUTH コマンド
      パスワードで保護された領域へのアクセス要求に先行してパスワード認証を要求する。ここで肯定応答である PACK が得られれば所定のアクセスが可能となる(PACK の値はデフォルト 0x0000 。変更可)
      • リクエスト
        • コマンド 1 バイト(1Bh)+ パスワード 4 バイト + CRC 2 バイト
      • レスポンス
        • OK: PACK 2 バイト
        • NG: NAK 各値

2. Android API による NTAG21x のハンドリングについて

手順等

  • NTAG21x は ISO/IEC 14443 Type A であるため Android でアクセス制御機構を利用する際には標準の NfcA クラスを使用する。なお MIFARE Ultralight に特化した MifareUltralight クラスも存在するがここでは前掲のネイティブコマンド操作が処理の中心であるためあえて同クラスを利用する必然性はない
  • 所定のタグの近接を検知したらまず Tag クラスの getTechList メソッドにより当該タグの TagTechnology 文字列リストを取得。リストに "android.nfc.tech.NfcA" が含まれていれば NfcA クラスの get メソッドで NfcA オブジェクトを取得し transceive メソッドにより適宜 NTAG コマンドを呼び出すことで製品の識別と必要な処理を行う。ターゲットが NTAG21x と識別されない場合は後続処理をキャンセル
  • NfcA - API level 10
    public final class NfcA

    Provides access to NFC-A (ISO 14443-3A) properties and I/O operations on a Tag.

    Acquire a NfcA object using get(Tag).

    The primary NFC-A I/O operation is transceive(byte[]). Applications must implement their own protocol stack on top of transceive(byte[]).
    • get - API level 10
      NfcA get (Tag tag)

      Get an instance of NfcA for the given tag.

      Returns null if NfcA was not enumerated in getTechList(). This indicates the tag does not support NFC-A.

      Parameters
      • tag Tag: an NFC-A compatible tag
      Returns
      • NfcA NFC-A object
    • transceive - API level 10
      byte[] transceive (byte[] data)

      Get an instance of NfcA for the given tag.

      Send raw NFC-A commands to the tag and receive the response.

      Applications must not append the EoD (CRC) to the payload, it will be automatically calculated.


      Applications must only send commands that are complete bytes, for example a SENS_REQ is not possible (these are used to manage tag polling and initialization).

      Use getMaxTransceiveLength() to retrieve the maximum number of bytes that can be sent with transceive(byte[]).

      This is an I/O operation and will block until complete. It must not be called from the main application thread. A blocked call will be canceled with IOException if close() is called from another thread.

      Parameters
      • data byte: bytes to send
      Returns
      • byte[] bytes received in response
      Throws
      • IOException if there is an I/O failure, or this operation is cance

コードを書いて試す

  1. パスワードを設定する(write 要求からの保護)
    • NTAG21x 種別を確認
    • パスワードとして "0000" を設定
    • ページ 04h 以降を保護対象とする
              :
      
          if (isNfcA) {
            int ntagMaxPage = -1, ntagConfPage0;
            NfcA nfca = NfcA.get(tag);
            try {
              nfca.connect();
            } catch (IOException e) {
              Log.d(TAG, "NfcA.connect() err: " + e.toString());
              return;
            }
            // NTAG 種別取得
            try {
              byte[] res = nfca.transceive(new byte[]{
                (byte) 0x60, // GET_VERSION
              });
              Log.d(TAG, "GET_VERSION reslen=" + res.length + " res=" + bytesToHexString(res));
              if (res.length == 8) {
                if (res[0] == 0x00 && res[1] == 0x04 &&
                    res[2] == 0x04 && res[3] == 0x02) {
                  byte ntagStorageSize = res[6];
                  if (ntagStorageSize == 0x0F) {
                    ntagMaxPage = 45; // 0x2D = NTAG213
                  } else if (ntagStorageSize == 0x11) {
                    ntagMaxPage = 135; // 0x87 = NTAG215
                  } else if (ntagStorageSize == 0x13) {
                    ntagMaxPage = 231; // 0xE7 = NTAG216
                  }
                }
              }
            } catch (IOException e) {
              Log.d(TAG, "GET_VERSION err:" + e.toString());
            }
            if (ntagMaxPage == -1) {
              Log.d(TAG, "is not NTAG21x");
              if (nfca.isConnected()) {
                try {
                  nfca.close();
                } catch (IOException e) {
                  Log.d(TAG, "NfcA.close() err: " + e.toString());
                }
              }
              return;
            }
            // Congiguration Pages 開始位置
            ntagConfPage0 = ntagMaxPage - 4;
      
            try {
              // パスワードを設定
              int page = ntagConfPage0 + 2; // config page 2
              byte[] res = nfca.transceive(
                  new byte[]{
                      (byte) 0xA2, // WRITE
                      (byte) page,
                      '0', '0', '0', '0'
                  }
              );
              Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
      
              // 保護開始ページを設定
              page = ntagConfPage0; // config page 0
              res = nfca.transceive(
                  new byte[]{
                      (byte) 0xA2, // WRITE
                      (byte) page,
                      (byte) 0x04,
                      (byte) 0x00,
                      (byte) 0x00,
                      (byte) 0x04 // 保護開始ページ
                  }
              );
              Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
            } catch (IOException e) {
              Log.d(TAG, "WRITE err:" + e.toString());
            }
      
            if (nfca.isConnected()) {
              try {
                nfca.close();
              } catch (IOException e) {
                e.printStackTrace();
              }
            }
          }
              :
      
    • 上のコードにより NTAG213 のページ 04h 以降への書き込みにパスワード認証通過が必須となった状況
      AUTH0 に保護開始ページ指定
      PROT ビットはデフォルトの 0 のまま
         
  2. パスワードを設定する(read / write 要求からの保護)

    NTAG21x では write 要求のみならず read 要求に対してもパスワードによる保護が可能。ただしパスワードは両者共通

    • NTAG21x 種別を確認
    • パスワードとして "0000" を設定
    • ページ 04h 以降を保護対象とする
    • PROT ビットを立て read 要求からの保護も有効に
              :
      
          if (isNfcA) {
            int ntagMaxPage = -1, ntagConfPage0;
            NfcA nfca = NfcA.get(tag);
            try {
              nfca.connect();
            } catch (IOException e) {
              Log.d(TAG, "NfcA.connect() err: " + e.toString());
              return;
            }
            // NTAG 種別取得
            try {
              byte[] res = nfca.transceive(new byte[]{
                (byte) 0x60, // GET_VERSION
              });
              Log.d(TAG, "GET_VERSION reslen=" + res.length + " res=" + bytesToHexString(res));
              if (res.length == 8) {
                if (res[0] == 0x00 && res[1] == 0x04 &&
                    res[2] == 0x04 && res[3] == 0x02) {
                  byte ntagStorageSize = res[6];
                  if (ntagStorageSize == 0x0F) {
                    ntagMaxPage = 45; // 0x2D = NTAG213
                  } else if (ntagStorageSize == 0x11) {
                    ntagMaxPage = 135; // 0x87 = NTAG215
                  } else if (ntagStorageSize == 0x13) {
                    ntagMaxPage = 231; // 0xE7 = NTAG216
                  }
                }
              }
            } catch (IOException e) {
              Log.d(TAG, "GET_VERSION err:" + e.toString());
            }
            if (ntagMaxPage == -1) {
              Log.d(TAG, "is not NTAG21x");
              if (nfca.isConnected()) {
                try {
                  nfca.close();
                } catch (IOException e) {
                  Log.d(TAG, "NfcA.close() err: " + e.toString());
                }
              }
              return;
            }
            // Congiguration Pages 開始位置
            ntagConfPage0 = ntagMaxPage - 4;
      
            try {
              // パスワードを設定
              int page = ntagConfPage0 + 2; // config page 2
              byte[] res = nfca.transceive(
                  new byte[]{
                      (byte) 0xA2, // WRITE
                      (byte) page,
                      '0', '0', '0', '0'
                  }
              );
              Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
      
              // 保護開始ページを設定
              page = ntagConfPage0; // config page 0
              res = nfca.transceive(
                  new byte[]{
                      (byte) 0xA2, // WRITE
                      (byte) page,
                      (byte) 0x04,
                      (byte) 0x00,
                      (byte) 0x00,
                      (byte) 0x04 // 保護開始ページ
                  }
              );
              Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
      
              // read 要求からの保護も有効に
              page = ntagConfPage0 + 1; // config page 1
              res = nfca.transceive(
                  new byte[]{
                      (byte) 0xA2, // WRITE
                      (byte) page,
                      (byte) 0x80, // read,write protect
                      (byte) 0x05,
                      (byte) 0x00,
                      (byte) 0x00
                  }
              );
              Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
            } catch (IOException e) {
              Log.d(TAG, "WRITE err:" + e.toString());
            }
      
            if (nfca.isConnected()) {
              try {
                nfca.close();
              } catch (IOException e) {
                e.printStackTrace();
              }
            }
          }
              :
      
    • 上のコードにより NTAG213 のページ 04h 以降の読み書きにパスワード認証通過が必須となった状況
         
  3. パスワードを解除する
    • NTAG21x 種別を確認
    • PWD_AUTH コマンドでパスワード "0000" を提示し認証要求
    • AUTH0 に有効範囲外のページ番号 FFh を書き込むことでパスワード保護を無効化する
              :
      
          if (isNfcA) {
            int ntagMaxPage = -1, ntagConfPage0;
            NfcA nfca = NfcA.get(tag);
            try {
              nfca.connect();
            } catch (IOException e) {
              Log.d(TAG, "NfcA.connect() err: " + e.toString());
              return;
            }
            // NTAG 種別取得
            try {
              byte[] res = nfca.transceive(new byte[]{
                (byte) 0x60, // GET_VERSION
              });
              Log.d(TAG, "GET_VERSION reslen=" + res.length + " res=" + bytesToHexString(res));
              if (res.length == 8) {
                if (res[0] == 0x00 && res[1] == 0x04 &&
                    res[2] == 0x04 && res[3] == 0x02) {
                  byte ntagStorageSize = res[6];
                  if (ntagStorageSize == 0x0F) {
                    ntagMaxPage = 45; // 0x2D = NTAG213
                  } else if (ntagStorageSize == 0x11) {
                    ntagMaxPage = 135; // 0x87 = NTAG215
                  } else if (ntagStorageSize == 0x13) {
                    ntagMaxPage = 231; // 0xE7 = NTAG216
                  }
                }
              }
            } catch (IOException e) {
              Log.d(TAG, "GET_VERSION err:" + e.toString());
            }
            if (ntagMaxPage == -1) {
              Log.d(TAG, "is not NTAG21x");
              if (nfca.isConnected()) {
                try {
                  nfca.close();
                } catch (IOException e) {
                  Log.d(TAG, "NfcA.close() err: " + e.toString());
                }
              }
              return;
            }
            // Congiguration Pages 開始位置
            ntagConfPage0 = ntagMaxPage - 4;
      
            // パスワード認証
            // 通過すれば close するまで権限が持続
            boolean authOk = false;
            try {
              byte[] res = nfca.transceive(
                  new byte[]{
                      (byte) 0x1b, // PWD_AUTH
                      '0', '0', '0', '0'
                  }
              );
              Log.d(TAG, "PWD_AUTH reslen=" + res.length + " res=" + bytesToHexString(res));
              if (res.length == 2) {
                authOk = true;
              }
            } catch (IOException e) {
              Log.d(TAG, "PWD_AUTH err:" + e.toString());
            }
      
            if (authOk) {
              // パスワード保護を解除
              try {
                int page = ntagConfPage0; // config page 0
                byte[] res = nfca.transceive(
                    new byte[]{
                        (byte) 0xA2, // WRITE
                        (byte) page,
                        (byte) 0x04,
                        (byte) 0x00,
                        (byte) 0x00,
                        (byte) 0xFF // 有効範囲超のページ指定で保護は無効に
                    }
                );
                Log.d(TAG, "WRITE p" + page + " reslen=" + res.length + " res=" + bytesToHexString(res));
              } catch (IOException e) {
                Log.d(TAG, "PWD WRITE err:" + e.toString());
              }
            }
      
            if (nfca.isConnected()) {
              try {
                nfca.close();
              } catch (IOException e) {
                Log.d(TAG, "NfcA.close() err: " + e.toString());
              }
            }
          }
              :
      
    • 上のコードにより前掲 2.のコードによるパスワード保護をこのコードで解除した状況
      ここでは PWD 自体には手をつけておらず PROT ビットも 1 のままだが、AUTH0 = FFh につきユーザ領域を自由に読み書き可能な状態に戻っている
         

3. 試作した Android アプリについて

NTAG で秘密情報を扱うという考え方

ここまで見てきたように NTAG21x はパスワード設定により Write 要求のみならず Read 要求を弾くことも可能です。これは興味深い機能ですが、両者のパスワードが共通であるためたとえば次のような取り回しはできません。

  • 部外者: タグデータへのアクセス不可
  • 利用者: Read パスワードによりタグデータの読み出しが可能
  • 管理者: 読み出しに加え Write パスワードでタグデータの書き換えが可能

このように複数のロールでタグを共用したい場合ではなく、所定のタグを独占的・排他的に扱いたい場合にこそこの機能は有用でしょう。その典型的な使用例として秘密情報の格納を想起しました。以下の発想によるものです。

  • デバイスのストレージも公共のネットワークも使用しないため情報漏洩のリスクが極めて小さい
  • 薄くて小さく軟弱な媒体であるためマスターデータの保存には不向きだが、逆にデータのコピーを携行する目的には好適であり、その状況下では簡単に媒体を破壊・破棄できることが利点にもなる
  • NFC の特性上表面を覆っても読み書きができるため手近なものに忍ばせることも容易
  • パスワードで対 Read 保護を設定しておけば他者の手に渡っても一般的なリーダーやアプリではデータを読み出せない。仮に NTAG パスワード認証を試みられたとしても事前に上限回数を設定しておけば試行の過程で当該タグは永久に封鎖される
  • 記録容量の少なさはデータ圧縮と複数タグへの分割格納で補うことが可能。嵩張らないため複数枚に渡っても携行性への影響は微小

なお、こういった使途においては NFC の世界で「データ交換」を行う上での便宜としての NDEF 形式を使用する必然性はありません。ユーザメモリ全体を任意の形式で効率よく取り回せばよいでしょう。

試作中の情報収集時に、秘密情報を紙媒体で管理する「パスワードノート」という製品があることを知りました。

ストレージやネットワークを使用しない点が共通しており興味を持ちました。たしかに「手書き」には他の方法では決して及ばない様々な柔軟性があります。これがヒントとなって NTAG を手帳へ貼付して使うアイディアに至りました。ふたつの文化の利点を活用できる良い組み合わせだと思います。

実装内容

試作の仕様として以下の内容を想定しました。

  1. NTAG213, NTAG216 を対象とする
  2. NDEF は使用せず最小限のデータヘッダに続けてユーザメモリ一杯までデータを格納する
  3. テキスト形式のデータであることを前提とする。容量節約のためマルチバイト文字のエンコーディングには Unicode ではなく Shift-JIS を使用する
  4. データは gzip 形式で圧縮する
  5. データが一枚のタグに収まらない場合は複数のタグへ分割格納する。ただし、データを分割して格納したタグはそれぞれ単独での読み出しも可能とする(全タグの順次読み出しをデータ復元の前提とせず、全タグが揃わなくても部分データを柔軟に取得できるようにする)
  6. Read/Write 要求に対しメモリの全ページをパスワード保護対象とする
  7. NTAG の 4 バイトのパスワード領域へ 4 バイトの ASCII キャラクタコードを格納するのではなく指定されたパスワード文字列の CRC32 値を格納する (PWD のバリエーションは 256^4 となる)
  8. 上記の措置のため他のツールではここで設定したパスワード保護を解除不能につき解除機能を独自に実装する必要がある
  9. 総当たり攻撃への対処としてパスワード認証の試行回数に上限値(AUTHLIM)を設定することにより試行回数超過の場合には NTAG の自爆機構(PWD_AUTH 要求に対する無条件拒否)を発動させる
  10. 上記にも関連し、ここではマスターデータの保存を目的とするのではなく、コピーデータの便宜的な保持を目的とすることを前提とする

NTAG ユーザメモリの使いかた

 ========= ユーザメモリ領域全体の構成 =========

 1. page 04h - 05h に固有の管理情報を格納する
 2. page 06h 以降に gzip データを格納する

 ========= 1. 管理情報について =========

 [page 04h]
    0    1    2    3
  +----+----+----+----+
  |'t' |'t' | -- | -- |
  +----+----+----+----+

  第 1, 2 バイト
    識別子 "tt"

  第 3, 4 バイト
    予備
    ※当初、データを分割格納したタグセットの識別用領域と
      することを想定したが費用対効果に乏しいと判断し中止

 [page 05h]
    0    1    2    3
  +----+----+----+----+
  | NN | -- | NN | NN |
  +----+----+----+----+
  
  第 1 バイト
   最上位ビット:後続タグの有無  0=後続なし 1=後続あり
   下位 4 ビット:タグ連番(0h - Fh)

  第 2 バイト
   予備

  第 3, 4 バイト
   ページ 06h 以降に格納ずみの gzip データサイズ
   short ビッグエンディアン

 ========= 2. gzip データについて =========

  - テキストデータの gzip 圧縮はオンメモリで行う

  - タグへ書き込む際には gzip データの半固定ヘッダ
    10バイト(*)を除去し、読み出しの際には当該ヘッダを
    補填した上で unpack する
    (*) http://www.onicos.com/staff/iz/formats/gzip.html

  - データを複数のタグへ分割格納する場合は上記の管理情報で
    連番管理を行うが、事後に各タグから単独で部分データを
    読み出すことも可能とするために、タグへ書き込むのは
    「元データ全体を圧縮した gzip データの一部分」ではなく
    「元データを適切な位置で分割して圧縮した gzip データ」とする

ソースコード

※本記事の冒頭でも触れたように NTAG21x のアクセス制御機構の扱いには十分な注意が必要です。 試作のソースコード公開はあくまでも技術情報の紹介を目的とするものでありプログラム実行時の動作は保証しません。これを使用して何らかの損害が発生したとしても当方は一切の責任を負いません。

動作の様子

以下のデモ動画ではテストデータとして次の 4,978 バイトの英単語リストを使用。このデータを圧縮して 3 枚の NTAG216 に分割格納しています。

START, ability, abroad,
accept, access, accident,
according, account, action,
activity, actually, add,
addition, additional, address,
adult, advance, advanced,
advantage, advice, advise,
age, agency, agent,
agree, ahead, air,
airline, allow, amount,
angry, announce, announcement,
anxious, appear, appearance,
application, apply, approach,
area, arrange, arrangement,
arrival, arrive, article,
attack, attend, attention,
available, average, avoid,
aware, balance, balanced,
bar, base, basic,
bear, beat, beauty,
benefit, bill, bit,
block, blood, board,
borrow, boss, branch,
break, broad, broadcast,
brush, budget, burn,
business, busy, cab,
cable, call, cancel,
capital, care, careful,
case, cash, catalog,
catch, cause, century,
certain, certainly, chance,
change, charge, chart,
cheap, check, chemical,
choose, citizen, claim,
clear, clerk, close,
clothes, collect, collection,
comfortable, common, communication,
company, compare, complain,
complete, concern, condition,
contact, contain, content,
continue, contract, control,
convenient, conversation, copy,
corner, correct, cost,
count, couple, course,
court, cover, crash,
create, credit, cross,
crowd, crowded, customer,
daily, damage, data,
date, deal, decide,
decision, defence, degree,
delay, deliver, demand,
department, depend, deposit,
describe, design, destroy,
detail, develop, dial,
diet, difference, difficulty,
direct, direction, directly,
director, discount, discuss,
disease, disk, display,
distance, district, document,
double, doubt, downtown,
dress, drive, drop,
drug, due, earn,
earthquake, economy, edge,
education, effect, effective,
effort, electricity, employee,
empty, encourage, energy,
enjoy, enter, entrance,
equipment, event, examination,
example, excellent, except,
exciting, excuse, exercise,
exit, expect, expense,
expensive, experience, expert,
explain, express, extra,
face, fail, fair,
fall, fan, fare,
fashion, fault, favor,
favorite, feature, fee,
fence, figure, file,
fill, film, final,
fine, fire, firm,
fit, fix, flag,
flat, flood, floor,
flow, follow, following,
force, foreign, form,
former, forward, found,
foundation, free, freeze,
front, fund, furniture,
further, future, gain,
garage, gas, gather,
general, global, goal,
government, grade, graduagte,
grand, ground, guard,
guess, guide, handle,
hang, happen, hardly,
head, heart, heat,
highly, hire, hold,
host, human, hurt,
illness, image, immediately,
improve, include, income,
increase, individual, industry,
inform, information, insect,
instead, instruction, insurance,
intend, interest, international,
interview, introduce, invitation,
invite, issue, item,
job, join, judge,
knowledge, lack, land,
language, law, lawyer,
lay, lead, lend,
length, level, license,
lie, lift, likely,
limit, limitation, line,
list, load, loan,
local, location, lock,
lose, loss, luck,
lucky, luggage, mail,
main, major, majority,
manage, management, manager,
mark, market, master,
match, material, matter,
meal, means, measure,
media, medicine, mention,
message, method, mind,
minute, miss, mix,
model, modern, moment,
monthly, movemet, nation,
national, nearly, necessary,
notice, offer, office,
officer, official, operate,
operation, operator, opinion,
order, organization, own,
pack, package, pain,
part, party, pass,
passenger, patient, pay,
payment, per, performance,
performance, period, permit,
personal, pick, plain,
plan, plant, plate,
pleasure, plenty, point,
pole, policy, popular,
position, possible, post,
powerful, practice, prefer,
prepare, present, press,
prevent, price, print,
private, probably, problem,
process, produce, product,
production, professional, profit,
program, progress, project,
promise, property, protect,
protection, proud, provide,
public, publish, purpose,
puzzle, quality, quarter,
race, raise, range,
rapid, rate, reach,
ready, realize, reason,
receive, recent, recently,
recommend, record, reduce,
refer, regard, regarding,
regular, relationship, remember,
remove, rent, repair,
report, request, require,
research, reserve, responsible,
rest, result, return,
rise, room, route,
row, run, rush,
safe, safety, sail,
salary, sale, save,
schedule, seat, section,
security, sense, separate,
separately, serious, serve,
service, shake, shape,
share, ship, shock,
short, show, sign,
signal, single, situation,
skill, skin, slip,
social, solve, sort,
sound, spare, spend,
spot, spread, staff,
stage, stair, stand,
standard, state, statement,
step, stock, store,
storm, stretch, strike,
study, subject, submit,
succeed, success, suffer,
suggest, suit, supply,
support, suppose, sure,
surprise, survey, swing,
system, taste, tax,
temperature, terible, term,
tie, tight, tip,
tired, tool, total,
touch, tour, trade,
traffic, training, trip,
trouble, turn, type,
usual, valuable, value,
view, vote, want,
war, warn, way,
wear, weather, win,
wind, wire, wise,
wonder, worth, worthy,
xenophobia, yard, yarn,
yawn, yearn, yell,
yellow, yesterday, yet,
yield, yolk, young,
youth, zeal, zenith,
zigzag, zinc, zoom,
END

動画:5分1秒

※パスワード認証試行上限回数を超過した状況の動画はこちら


(tanabe)
klab_gijutsu2 at 17:00|この記事のURLComments(0)
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント