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 ntagStrorageSize = res[6];
                  if (ntagStrorageSize == 0x0F) {
                    ntagMaxPage = 45; // 0x2D = NTAG213
                  } else if (ntagStrorageSize == 0x11) {
                    ntagMaxPage = 135; // 0x87 = NTAG215
                  } else if (ntagStrorageSize == 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 ntagStrorageSize = res[6];
                  if (ntagStrorageSize == 0x0F) {
                    ntagMaxPage = 45; // 0x2D = NTAG213
                  } else if (ntagStrorageSize == 0x11) {
                    ntagMaxPage = 135; // 0x87 = NTAG215
                  } else if (ntagStrorageSize == 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 ntagStrorageSize = res[6];
                  if (ntagStrorageSize == 0x0F) {
                    ntagMaxPage = 45; // 0x2D = NTAG213
                  } else if (ntagStrorageSize == 0x11) {
                    ntagMaxPage = 135; // 0x87 = NTAG215
                  } else if (ntagStrorageSize == 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)NFC | Android
2017年12月25日

外部向け HTTP 通信の再送検知ツールを作った話

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

このエントリは KLab Advent Calendar 2017 の最終日の記事です。

こんにちは。インフラ担当 高橋です。

このエントリでは、つい先日、少し変わった仕組みの外部向け HTTP 通信の 3way ハンドシェイクの再送検知ツールを作りましたのでご紹介します。

きっかけ

このツールを作ったのは、DSAS の外部で発生するネットワーク障害を検知できるようにするためです。

例えばこのような障害がありました。

DSAS は複数のデータセンタで稼働しているのですが、ある日、特定データセンタの Web サーバから外部 API への接続が重くなる事象が発生しました。

原因は外部ネットワークからの DDoS 攻撃により、データセンタのネットワークの一部区間が飽和状態になったことによるものでした。

外部ネットワーク障害 その1

またある日は、特定のデータセンタの Web サーバから外部の特定 API に接続ができなくなりました。

原因は外部ネットワークの途中経路の ISP で伝送装置が故障したことで、経路変動が発生し、通信が不安定になったことによるものでした。

外部ネットワーク障害 その2

このように DSAS 外部で発生するネットワーク障害の原因は様々であり、対応も状況によって都度変化します。DSAS 内のネットワーク、データセンタの回線、その先の途中経路、外部 API のサーバなど、切り分けしなければならない箇所は多くあります。

特に途中経路の障害というのは、データセンタの回線や外部 API には問題が発生しておらず、障害箇所の切り分けにも時間がかかります。

このような障害はサービスへの影響に直結するため、インフラ担当はできるだけ早く障害に気づき、対応を開始しなければなりません。

そのため、インフラ担当が DSAS 外部で発生するネットワーク障害を検知できる仕組みを導入することにしました。

どのようにして検知するか

障害を検知するには、障害が発生しそうな箇所を想定し、そこを監視する必要があります。

DSAS 内部とは異なり、DSAS 外部で発生するネットワーク障害の原因は千差万別で、且つ監視できる箇所は限られてきます。

何を検知するか

まずは、先ほど例に挙げた障害を振り返ってみることにします。

このような障害の場合、DSAS のサーバやスイッチは正常なためアラートは発生しません。案件担当からの「外部 API の応答が遅い、もしくはタイムアウトしてエラーになる」といった問い合わせがきっかけで障害に気づくことになります。

つまり、外部 API への HTTP 接続の応答が遅くなったり、タイムアウトが発生していることをきっかけに障害を検知しているということになります。

それでは、外部 API への HTTP 接続の応答時間が長ければネットワーク障害が発生しているかというと、外部 API の処理に時間がかかっている可能性もあり、そうとは言い切れません。

このような障害が発生すると、インフラ担当は モニタリングシステム (Ganglia) のグラフを確認します。

グラフには HTTP の外向きの通信についての項目があり、該当時刻に Web サーバで SYN パケットの再送が大量に記録されていました。

HTTP 通信の SYN パケットの再送が起きているということは、外部 API に SYN パケットが届かなかったり、外部 API からの SYN/ACK パケットが Web サーバに届かなかったりしている可能性が高いです。

これを監視すれば DSAS 外部のネットワーク障害を検知できそうです。

何を使って検知するか

直接、監視サーバから外部への HTTP 監視を行うには、ヘルスチェックのような監視用 URL が必要になりますが、インフラ担当では用意することができませんので、案件担当と連携して監視対象 URL のリストを作成し、管理しなければなりません。

DSAS では複数案件のサービスが稼働しており、その全ての案件のリストを管理するのは大変ですので、それは避けたいという思いがありました。

それに加えて今回は、HTTP 接続の応答時間やステータスコードではなく、SYN パケットの再送を監視する必要があります。

「監視対象 URL を管理せずに、外部向け HTTP 通信の SYN パケットの再送を検知したい」

この要望を叶えることのできるツールが、意外と身近にあることに気づきました。

モニタリングシステムのグラフ作成に使用している tcpeek です。

tcpeek とは

tcpeek は KLab が作成した 3way ハンドシェイク時に発生するエラーを監視・集計するネットワークモニタで、エラー検出、再送検出、フィルタ、データ出力といった機能を備えています。

GitHub - pandax381/tcpeek: TCP 3way-handshake monitor

こちらのエントリに詳細が書いてあります。

ログからは見えてこない高負荷サイトのボトルネック

再送検出

指定したインタフェースを監視し、タイムアウト時間内に SYN の再送が発生すると SYN Segment Duplicate (dupsyn)、接続先から SYN/ACK が再送されてくると S/A Segment Duplicate (dupsynack) がカウントされます。

タイムアウト時にも再送は発生していますが、tcpeek の再送検出は 3way ハンドシェイク成功時に再送が発生していればカウントされるようになっています。

想定される原因としては、途中経路の帯域の輻輳やパケ落ちなどが挙げられます。

エラー検出

指定したインタフェースを監視し、RST パケットを受け取ると Connection Rejected (reject)、ICMP Destination Unreachable パケットを受け取ると ICMP Unreachable (unreach)、接続がタイムアウトすると Connection Timed Out (timeout) がカウントされます。

タイムアウトについては、アプリケーションによりタイムアウトまでの時間は異なりますので、tcpeek 起動時に指定するタイムアウト時間 (デフォルト 60秒) を超えたセッションは Connection Timed Out としてカウントされるようになっています。

想定される原因としては、接続先の TCP リセット、途中経路のルーティングテーブルの消失、接続先の無応答などが挙げられます。

tcpeek を使った SYN パケット再送検知の仕組み

tcpeek を使えば外部向け HTTP 通信に絞った 3way ハンドシェイクの再送発生 (dupsyn / dupsynack) や接続失敗 (reject / unreach / timeout) の計測値を取得することができますので、そのカウントが増えたらインフラ担当に通知するツールを作成することにしました。

====== TCPEEK SUMMARY ======
----------------------------
 http-out
----------------------------
 success
     total           17457
     dupsyn              3
     dupsynack           0
 failure
     total             113
     reject             39
     unreach            45
     timeout            29
============================

tcpeek は Web サーバで動作しますので計測値は Web サーバで取得することになります。

Web サーバから直接インフラ担当に通知する仕組みだと、多数の Web サーバで HTTP の再送が発生すると通知の数が大量になってしまいます。

そのため、Web サーバの計測値をキャッシュに格納して集約し、検知ツールでキャッシュにある統計情報をチェックする仕組みにしました。

監視の仕組み

計測値のカウントが増えており、その数がしきい値を超えていると slack とメールでインフラ担当に通知します。こちらは slack への通知例です。

slack通知

おわりに

今回、このツールを作る際に一番悩んだのが、通知内容としきい値の設定方法でした。

Web サーバ単位でカウントを通知すると、台数が多いと通知が縦に長くなってしまい、特に slack では扱いづらくなるため、案件単位で通知をまとめるようにしました。

また、Web サーバ毎にしきい値を設定すると、特定の接続先の障害で再送などが発生した場合、何台の Web サーバで再送が発生しているのか状況を把握しづらくなるため、案件の Web サーバ全台の合計カウント数をしきい値としています。

この 2点はまだまだ改善の余地があると思っており、これからもブラッシュアップしていきたいです。

もちろん tcpeek は外部向け HTTP 通信だけではなく、様々な通信のフィルタを作成し集計できますので、他の通信に対してもこの検知の仕組みは適用できます。

takahashi_yo at 12:48
この記事のURLComments(0)network | 開発
2017年12月21日

Python 3.7 でテキストファイルのエンコーディングを初期化後に変更可能になります

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

Python のテキストファイル (厳密には io.TextIOWrapper) はいままでコンストラクタでしかエンコーディングを指定することができませんでした。

特に標準入出力 (sys.stdin, sys.stdout, sys.stderr) のエンコーディングを設定するには環境変数 PYTHONIOENCODING を利用するしかなく、アプリケーションが設定ファイルなどに基づいてこれらのエンコーディングを変更するには sys.stdin を sys.stdin.fileno() や sys.stdin.buffer から作り直すなどのハック的な方法しか使えませんでした。

Python 3.7 からは TextIOWrapper に reconfigure() メソッド メソッドが追加され、一部のコンストラクタで設定するオプションを変更可能になります。 そして昨日このメソッドに encoding, errors, newline のサポートを追加しました。 (コミット)

これにより sys.stdin.reconfigure(encoding='EUC-JP') のようにしてエンコーディングを変更することができます。

Python 3.7 は PEP 538 と 540 によって UTF-8 を使う限りにおいてはほぼ不満ない状態になったと思いますが、これで UTF-8 以外が必要な場合についても「one obvious way」を提供できるようになったと思います。

注意点として、read側については、少しでも read していると変更できず、エラーになります。これは TextIOWrapper 内部で、下側(bufferd IO)からある程度のかたまりでバイト列を受け取り、それをバッファに置いてデコードして改行で区切って返しているので、とくに状態をもつエンコーディングのデコーダを使っている場合に「返したところまで」と「それ以降」のバイト単位での区切りをつけるのが難しいからです。

書き込み側については、単に flush() してから新しいエンコーディングのエンコーダを作り直せばいいので、途中からのエンコーディングの変更にも対応しています。


@methane

songofacandy at 17:40
この記事のURLComments(0)Python 
2017年12月18日

更新頻度の多いデータのキャッシュ

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

@methane です。

ISUCON 7 本戦で最大のスコアアップできたポイントが、 status と呼ばれる重い計算の結果となるJSONのキャッシュでした。

近年のISUCONによくある、「更新が成功したら以降のレスポンスにはその更新が反映される必要がある」(以降は「即時反映」と呼びます)タイプの問題だったのですが、今回のように更新頻度の高くかつ即時反映が求められるデータをキャッシュする方法について、より一般的に解説しておきたいと思います。

即時反映が不要な場合

まずは基本として、即時反映が不要な場合のキャッシュ方法からおさらいします。この場合、一番良く使われるのは参照時に計算した結果を Memcached などにキャッシュし、時間で expire する方法です。

このタイプのキャッシュには、参照元が分散している場合(Webサーバーが複数台あるなど)に Thundering Herd という問題がつきものです。参照頻度が非常に高く、かつ並列して行われる場合に、キャッシュが無効化した瞬間に(キャッシュで回避しているはずの)重い処理が並列で実行されてしまうという問題です。

Thundering Herd を避ける方法としては、memcached や MySQL を使って排他制御をするとか、 expire 時間が到達する前にランダムな時間をずらして投機的に再計算するという方法があります。

また、少しサーバー構成を複雑にしていいのであれば、Webサーバー以外でバックグラウンドに処理を実行して、そこで定期的にキャッシュを更新するという方法もあります。

最初のISUCONはブログがお題で、最新のブログコメントの一覧がサイドバーとしていろんなページに表示されていました。このサイドバーを作るケースなどにはバックグラウンド処理が非常に有効なはずです。

即時反映が必要&更新頻度が低い場合

即時反映が求められる場合は、更新処理の最後(例えばPOSTリクエストに対する処理のうち、MySQLにコミットした後、HTTPレスポンスを返す前)にキャッシュの無効化か更新をしてしまうという手があります。

無効化と更新のどちらがいいかは参照頻度で決まります。参照頻度が更新に比べて十分に高い場合、キャッシュを更新することで Thudering Herd を回避する事ができます。一方で参照頻度が十分高くない場合、一度も参照されないキャッシュを計算するのに時間とメモリを使ってしまう危険もあります。

即時反映が必要&更新頻度が高い場合

さて、本題です。結果をキャッシュしたい計算の重さに対して、参照頻度も更新頻度も非常に高い場合はどうすれば良いでしょうか?

もちろん、計算回数が更新回数よりも少なくなるようにしなければなりません。更新のたびにキャッシュを無効化したり再計算するのはダメです。

即時反映が不要な場合の方法を振り返ってみると、参照時に(Thundering Herdを避けつつ)計算する方法でも、バックグラウンドで非同期にキャッシュを更新する方法でも、計算頻度は更新頻度に影響されませんでした。キャッシュの再計算が1秒おきなら、その1秒の間に何千回の更新処理が走っていても関係ありません。なので、あとは即時反映の要求を満たすようになにか工夫するだけです。

たとえば、更新のたびに単調増加するバージョン番号のようなもの(MySQLのAUTO INCREMENTなIDなど )を用意します。参照時に計算をするなら、まず現在のバージョン (v0) を取得してからロックを取得し、ロックを取得できた時点で得られたキャッシュについているバージョンが v0 より新しければそのまま利用する、古ければ再度バージョン (v1) を取得し直して計算し、結果に v1 を付けてキャッシュするという手がとれます。

バックグラウンドで計算するなら、 redis の pubsub などを使って新しいバージョンのキャッシュが作られるのを待つという手も利用できるでしょう。

今回のISUCONでは、同じ status を共有するクライアントを同じプロセスに誘導できるようになっていたので、 Redis や MySQL を使わずにもっと楽に実装することができたはずです。

「まとめて処理」を「待つ」パターン

複数の更新処理に対して1度にまとめて重い処理を実行するという考えかたは、キャッシュに限らず広く有効なものです。例えば「日次バッチ」などと呼ばれる処理は大抵そうでしょう。最近のMySQLは複数のトランザクションのコミットを一度のディスクへの sync でまとめて実行すること(グループコミット) ができます。Fluentdが高いスループットを出せるのも、ある程度の量のイベントをバッファに貯めて一括で転送する設計が寄与していると思います。

そしてこの「まとめて処理」パターンには待ち時間がつきものです。日次バッチならそのバッチが終わるまで結果は見えませんし、トランザクションのコミットはグループコミットを待たされます。 「即時反映が必要でかつ更新頻度が高い」問題でのキャッシュが難しいのは、他のケースのキャッシュではこの「待ち時間」が必要ない、あるいは意識することがないからだと思います。

@tagomoris さんいわく "ISUCON参加前と参加後に最も多くのものを持ち帰った人こそが勝者と言えるでしょう。" (引用元) ということですが、参加チームの方にこの「まとめて処理」を「待つ」パターンを持ち帰って将来何かに役立ててもらえたら幸いです。

songofacandy at 21:12
この記事のURLComments(0)ISUCON 
2017年12月13日

最近のPython-dev(2017-12)

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

バックナンバー: 9月号 | 8月号 | 6月号 | 5月号 | 4月号 | 3月号 | 2月号 | 1月号

@methane です。 ISUCON があってしばらく間が空いてしまいました。コミットやML上の議論も追えてないのですが、1月末にPython 3.7のbeta1 (=feature freeze)が予定されているために、Python 3.7 を目標にしている PEP たちがたくさんacceptされたので、それらを紹介しておきます。

PEP 540 UTF-8 mode

https://www.python.org/dev/peps/pep-0540/

PEP 538 (locale coercion) とセットで、私が BDFL-delegate (PEP を accept する責任者) になった PEP です。

この PEP は当初はかなりのボリュームが有ったのですが、すでに PEP 538 を accept したので、それを補完する機能として大幅にシンプル化しました。

PEP 538 では、起動時に locale が C であったときに、 LC_CTYPE を C.UTF-8 などへの変更を試みます。 また、C locale では標準入出力のエラーハンドラが surrogateescape になり、例えば stdin から読んだ文字列をそのまま stdout に書く場合などに非ASCII文字に対してもバイト透過な振る舞いをするのですが、それを C.UTF-8 などの coercion ターゲットとなる locale にも適用します。

PEP 540 も、 locale を変更しない以外は全く同じ振る舞いをするようになりました。具体的には次のとおりです。

  • stdin, stdout の encoding/error handler が UTF-8/surrogateescape になる
  • sys.getfilesystemencoding() と locale.getpreferredencoding() が UTF-8 を返す

PEP 538 と違って実際の locale は変更されないので、例えば readline で日本語入力はできないままですが、C locale 以外存在しないコンテナ等で Python を動かすときにデフォルトでUTF-8を使ってほしいというような用途にはこれで十分です。

PEP 563 Postponed Evaluation of Annotations

https://www.python.org/dev/peps/pep-0563/

from __future__ import annotations を書くことで、関数アノテーションが評価されず、ただの文字列になります。とはいえ、ソースを読むときに構文としてはチェックされるので、任意の文字列がかけるわけではありません。

これにより、アノテーションを書くことによる性能のオーバーヘッドを減らす効果があるのと、アノテーション部分の名前解決のための forward references が不要になって書くのが楽になるという効果があります。

この動作は Python 4 からデフォルトになる予定なので、 Python 3.7 に移行した人は早めにこの動作を有効にすることをおすすめします。

個人的には、実行時に評価されなくなることで、Python の構文を実行時には許されない形で利用したり、あるいはアノテーション部分でしか利用できない構文を追加するという進化への道が開けたという点でも期待しています。例えば現在 Union[int, str] と書いている部分を int or str あるいは int | str と書けるようにする提案ができるかもしれません。(前者は評価するとただの int になり、後者は評価すると | が処理できずに TypeError になる)

PEP 560 Core support for typing module and generic types

https://www.python.org/dev/peps/pep-0560/

いままで type hint は Python 3 で追加された関数アノテーション以外には特別な Python に対する機能追加を必要としないように設計されてきましたが、 typing がある程度の成功を収めて来ているので、そろそろ typing の問題を解決するために Python 自体に手を入れてもいれようというのがこの PEP です。

例えば、 typing.Listclass List(list, MutableSequence[T], extra=list): ... として宣言されています。 この MutableSequence[T] の部分ですが、親クラスになるためにクラスでないといけないという制限があります。そのために実際に親クラスになってしまうので実際にメソッドを提供していないクラスが大量にMROに入りメソッド呼び出し性能のオーバーヘッドが大きくなるという問題があります。また、 MutableSequence 自体もクラスなので、それに対して [T] と書けるようにするためにメタクラスが使われています。

このために現在の typing は大量のメタクラスハックを必要とし、実行時オーバーヘッドもかかり、 import typing も遅くなり、また他のメタクラスとの衝突解決の手間が発生するという欠点を背負っています。

これを解決するために、 Python に次の機能を追加します。

  • class 文の親クラスリスト部分に、 type オブジェクトではない __mro_entries__ メソッドを持つオブジェクトを書くことができる
  • __class_getitem__ メソッドを定義すると、メタクラスを使わなくても MyClass[int] のようにクラスに添え字を書くことができる

これらの機能は typing モジュール以外から使えないというわけではありませんが、 typing 以外の用途での利用は非推奨になっています。

とはいえ、 __class_getitem__ については、最近のメタクラスを使わなくてもクラスの振る舞いをカスタマイズできる流れに添っていて黒魔術感も比較的少なめなので、本当にクラスオブジェクトに添え字アクセスが必要な場面であれば、typing 以外で使っても良いんじゃないかな。

PEP 561 -- Distributing and Packaging Type Information

https://www.python.org/dev/peps/pep-0561/

Typing が本格的に使われていくためには、サードパーティーライブラリの型情報をどうやって配布・利用するかを決めなければなりません。ということでそれを決めたのがこの PEP です。

PEP 562 -- Module getattr and dir

https://www.python.org/dev/peps/pep-0562/

モジュールに __getattr__ 関数を定義して遅延ロードや利用時warningなどを実現する仕組みです。

また、遅延ロードされる名前を提供するために __dir__ を利用することもできます。

個人的には、 import asyncio で芋づる式に multiprocessing まで import されているのを、 concurrent.futures.ProcessPoolExecutor を遅延ロードすることで解消したいと思っています。

PEP 565 -- Show DeprecationWarning in main

https://www.python.org/dev/peps/pep-0565/

Python には廃止予定のAPIについて警告するための DeprecatedWarning がありますが、これはPython製アプリケーションのユーザーにとってはほぼ無意味で混乱させるものなので、現在はデフォルトで表示されないようになっています。

しかし、 Python 開発者でもこの警告を有効にしていない人が多いために、DeprecationWarning に気づかれないという悩ましい状況も発生しています。

PEP 565 はこのバランスを少しだけ調整する提案です。 __main__ モジュールにおいてだけ、 DeprecationWarning をデフォルトで表示するようにします。

__main__ モジュールとは、インタラクティブシェルや Python インタプリタに渡された実行ファイルのことです。そこで直接廃止予定のAPIが呼ばれたときだけ DeprecationWarning が表示されるようになります。

これにより、開発者がAPIの使い方を調べるなどの目的でインタラクティブシェルで廃止予定のAPIを実行したときに Warning に気づけるようになると期待できます。

とはいえ、これの効果は限定的なので、Python開発者は -Wdefault オプションを使うか、 PYTHONWARNINGS=default と環境変数を設定しておきましょう。

-X dev option

https://docs.python.org/dev/using/cmdline.html#id5

上で紹介した -Wdefaultオプションに加えて、 Python 拡張モジュール開発者向けのものも含めて、幾つかの開発者向けオプションをまとめて有効にするオプションとして -X dev オプションが追加されました。

また、 PYTHONDEVMODE=1 という環境変数でも dev mode を有効にできるようになります。

PEP 557 -- Data Classes

https://www.python.org/dev/peps/pep-0557/

ちょうど Qiita に紹介記事があったのでそちらを参照してください。

Python3.7の新機能 Data Classes

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