LoginSignup
4
1

More than 1 year has passed since last update.

AndroidとiOSでSuicaを読み取る

Posted at

ICOCA やねんけどね

概要

  • 今回の目的
    Android と iOS で Suica を読み取ります。
  • 将来の目的
    Suica は NFC TypeF でマイナンバーカードは TypeB だそうです。役所系の案件を獲得できるかも知れません。

用語

  • NFC
    Near field communication の略で近距離無線通信と訳されています。主に非接触型カードに搭載され、近年のスマートフォンにも搭載されています。
  • Type
    運転免許証やマイナンバーカードなどに使用されています。
  • Type F (FeliCa)
    Suica PASMO Edy nanaco などに使用されています。
  • IDm
    カード固有の ID です。スマートフォンをピッって当てるだけで読み取れます。Suica で部屋の施錠などしていてカバンにぶら下げている人は危険かもしれません。
  • システムコード
    Suica は 0003 です。
  • サービスコード
    属性情報は 008B で利用履歴は 090F です。

環境

  • Flutter 3.0
  • nfc_manager 3.1.1

Androidの権限

android/app/src/main/AndroidManifest.xml に追記

<uses-permission android:name="android.permission.NFC" />

iOSの権限

  • iOS はデベロッパー登録が必要(らしい)
  • Info.plist に ISO 18092 system codes for NFC Tag Reader を追加 0003 を設定
  • Signing & Capabilities に Near Field Communication Tag Reading を追加

実機でエラーが出るとき

nfc.platformexception(io_exception,null,null,null)
  • リトルエンディアンに注意。
  • start して stop しないとスマホ本体が変な状態になります。再起動してください。
  • スマホ本体のNFCやおサイフケータイを有効にしてください。
  • 一度に取得できるデータ数は12個で2回に分ける必要があります。

Suica の読み取り実行結果

要求コマンドを送信すると16バイトのデータが返ってくるという流れです。

属性情報(008B)
00 00 00 00 00 00 00 00 32 00 00 B2 29 00 2E B1
利用履歴(090F) 最大20個
16 01 00 17 2D 64 8F 43 D9 52 B2 29 00 2E B1 A0
16 01 00 02 2D 64 FA B3 FA BE D4 2A 00 2E AF A0
1A 06 00 0E 2D 64 D9 52 D9 52 EC 2B 00 2E AD A0
C8 46 00 00 2D 63 5C 84 01 35 EC 2B 00 2E AB 00
C8 46 00 00 2D 62 A9 04 01 35 F4 2D 00 2E AA 00
16 01 00 17 2D 62 8F 43 D9 52 FC 2F 00 2E A9 A0
16 01 00 17 2D 62 D9 52 8F 43 1E 31 00 2E A7 A0
16 01 00 17 2D 61 8F 43 D9 52 40 32 00 2E A5 A0
16 01 00 17 2D 61 D9 52 8F 43 62 33 00 2E A3 A0
16 01 00 17 2D 5F 8F 43 D9 52 84 34 00 2E A1 A0

利用履歴の場合、日付が[04][05]で残高が[10][11]です。残高はリトルエンディアンで逆になります。

16 01 00 17 2D 64 8F 43 D9 52 B2 29 00 2E B1 A0

日付[04][05] = 2D 64 = 年7bit月4bit日5bit = 22/11/4
残高[10][11] = B2 29 = 0x29×256 + 0xB2 = 10674円
となります。

ソースコード

Github

Androidのみ(iOSはGithub参照)

main.dart
  /// 開始ボタン
  Future onStart() async {
    if(NfcManager.instance.isAvailable()==false){
      msg('NFC is not available');
      return;
    }
    msg('Please touch Suica');
    try {
      await NfcManager.instance.startSession(
        alertMessage: "Please touch Suica",
        onDiscovered: (tag) async {
          try {
            if (Platform.isIOS)
              await onDiscoveredForIos(tag);
            else if (Platform.isAndroid)
              await onDiscoveredForAndroid(tag);
          } catch (e) {
            msg('Error\n${e.toString()}');
            NfcManager.instance.stopSession(errorMessage: 'Error ${e.toString()}');
          }
          print('stopSession');
          NfcManager.instance.stopSession();
        },
      );
    } catch (e) {
      msg('Error\n${e.toString()}');
    }
  }

  /// Android向け(iOSはGithub参照)
  Future onDiscoveredForAndroid(NfcTag tag) async {
    final nfcf = NfcF.from(tag);
    if (nfcf == null) {
      msg('Unsupported card for NFC type F');
      return;
    }
    Uint8List IDm = nfcf.identifier;

    // 属性情報(008B)から残高を取得
    final list = [0x80,0];
    List<List<int>> res = await _readWithoutEncryption(
        nfcf:nfcf,
        IDm:IDm,
        serviceCode:[0x8b,0x00],
        blockCount:1,
        blockList:list);

    // 残高[11][12]
    int balance = -1;
    if(res.length>0)
      balance = res[0][12] * 256 + res[0][11];

    // 利用履歴(090F)から履歴20件を取得 ※一度に12件まで
    List<int> list1=[];
    for (int i=0; i<12; i++) list1.addAll([0x80,i]);
    List<List<int>> res1 = await _readWithoutEncryption(
        nfcf:nfcf,
        IDm:IDm,
        serviceCode:[0x0f,0x09],
        blockCount:12,
        blockList:list1);

    List<int> list2=[];
    for (int i=12; i<20; i++) list2.addAll([0x80,i]);
    List<List<int>> res2 = await _readWithoutEncryption(
        nfcf:nfcf,
        IDm:IDm,
        serviceCode:[0x0f,0x09],
        blockCount:8,
        blockList:list2);
    List<List<int>> blocklist = [...res1,...res2];

    String histories = '';
    for(List<int> b in blocklist) {
      // 年月日[4][5](7bit 4bit 5bit)
      int idate = b[4] * 256 + b[5];
      String y = ((idate & 0xFE00) >> 9).toString();
      String m = ((idate & 0x01E0) >> 5).toString().padLeft(2,'0');
      String d = ((idate & 0x001F) >> 0).toString().padLeft(2,'0');
      histories += '${y}-${m}-${d}';
      // 残高[10][11]
      histories += '  ' + (b[11] * 256 + b[10]).toString().padLeft(5,' ') + ' yen';
      histories += '\n';
    }

    String s = '';
    s += 'IDm ${intlist_to_string(nfcf.identifier)}\n';
    s += 'SystemCode ${intlist_to_string(nfcf.systemCode)}\n';
    s += 'Balance ${balance} yen\n';
    s += histories;
    msg(s);
  }

  /// コマンド Read Without Encryption (0x06) の送受信
  /// - nfcf Android 向け
  /// - IDm 固有ID 8バイト
  /// - serviceCode 属性情報(008B) 利用履歴(090F)
  /// - blockCount 受信ブロック数
  /// - blockList 受信ブロック
  /// - 戻り値 16バイトのリスト
  Future<List<List<int>>> _readWithoutEncryption({
    required NfcF nfcf,
    required List<int> IDm,
    required List<int> serviceCode,
    required int blockCount,
    required List<int> blockList }) async {
    List<int> cmd = [];
    cmd.add(0x00);           // コマンド長(後で)
    cmd.add(0x06);           // コマンドコード 06 Read Without Encryption
    cmd.addAll(IDm);         // IDm (8byte)
    cmd.add(0x01);           // サービス数
    cmd.addAll(serviceCode); // サービスコード
    cmd.add(blockCount);     // 受信ブロックの数
    cmd.addAll(blockList);   // 受信ブロック
    cmd[0] = cmd.length;     // コマンド長

    List<List<int>> blist = [];
    try {
      Uint8List res = await nfcf.transceive(data:Uint8List.fromList(cmd));

      // 応答データから16バイトのブロックリストを取得
      // [10] 0x00が成功
      // [12] 16バイトのブロックの数
      // [13] 以降16バイトのブロックが続く
      if (res.length >= 11 && res[10] == 0x00){
        int nblock = res[12];
        for (var i=0; i<nblock; i++){
          List<int> b = [];
          for (int j=0; j<16; j++){
            int k = 13 + (16*i) + j;
            if (res.length > k)
              b.add(res[k]);
          }
          blist.add(b);
          print(intlist_to_string(b));
        }
      } else {
        print('faild res.length ${res.length}');
        if(res.length>10) print('faild res[10] ${res[10]} (OK=0x00)');
      }
    } catch (e) {
      msg('Error:${e.toString()}');
      return blist;
    }
    return blist;
  }
}

これ余談なんですけど

JR が2024年から QR コードによる支払いを導入するそうです。今回の記事とは逆です。すべての駅に新型改札機を導入するのは大変だろうと思いましたが、スマホサイズの読み取り機があれば可能ですね。最近映画館でもQRコードによる発券をみたことがあります。しかし、イベント会場などでQRコードのワンタイム式にすると、インターネットに接続できない場合に使えないといったトラブルが起こります。
「ぴっ」ビジネスは黎明期です。
免許証やマイナンバーカードにもNFCが搭載されているので新しい可能性があります。読み取り機はスマホがあれば可能なので、中小企業でもチャンスがあるかも知れません。

参考にしたサイト

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1