Flutterでマイナンバーカードを読んでみた

## この記事について
この記事は [SUPER STUDIO Advent Calendar 2023](https://qiita.com/advent-calendar/2023/superstudio) の20日目の記事になります。

## マイナンバーを読んでみたい
会社のアドカレなのに、弊社のプロダクトと関係ないものを作って投稿するのもどうかと思うのですが、マイナンバーカードからマイナンバーを読んでみたいのです。

そう、ほら、あれだ、たとえばいつか転売対策とかでマイナンバーによるユーザー認証が必要になっちゃったりしなかったり、ECカートだってそういう需要があるかもしれないじゃないですか。なんとかしてマイナンバーカードからマイナンバーを読んでみたいのです。

…だって、**男女のマイナンバーを足して素数になるカップルをマッチングするマッチングアプリ**を作ってみたいと思ってしまったからです。

## マイナンバーカードの仕様について
マイナンバーカードのようなICチップを積んでいるICカードは、演算機能を持ったコンピューターのようなもので、単純な読み取りでもきちんとしたプロトコルに則って通信をする必要があります。

そのような仕様については@ishihattaさんの以下の記事が詳しいです。

https://qiita.com/ishihatta/items/7e541b64ade4bb3834da

こちらの投稿にも書いてありますが、個人情報の扱いには注意が必要ですし、パスワードロックやカード破損の恐れもありますので、くれぐれも自己責任でお願いします。

## FlutterでNFC通信を行う
NFC通信はiOSではCoreNFC、AndroidではNfcAdapterと、使うライブラリやアクセスの仕方が異なるので、Flutterのパッケージ「nfc_manager」を使います。

https://pub.dev/packages/nfc_manager

そして[Setup](https://pub.dev/packages/nfc_manager#setup)に記載の通り、Androidはマニフェストファイル、iOSはInfo.plistなどに必要な設定をしましょう。

…と、ここで今回のどハマリポイント。
iOSの設定が少しややこしかったです。

### Capabilitiesの設定

FlutterプロジェクトのiOSフォルダの`Runner.xcodeproj`をXcodeで開き、プロジェクトからTARGETSのSigning & Capabilitiesで、`Near Field Communication Tag Reading`を追加

![スクリーンショット 2023-12-18 22.37.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19193/684f0f95-b74e-a521-4d73-19be6442b871.png)

### Entitlementsの設定

Runner.entitlementsファイルを開き、`com.apple.developer.nfc.readersession.formats`に`TAG`を追加

```
	<key>com.apple.developer.nfc.readersession.formats</key>
	<array>
		<string>TAG</string>
	</array>
```

### info.plistの設定

info.plistを開き、
- `NFCReaderUsageDescription`に特権の説明
- `com.apple.developer.nfc.readersession.iso7816.select-identifiers`に4つのID（マイナンバーカードの指定）
- `com.apple.developer.nfc.readersession.felica.systemcodes`にIDをひとつ追加

```
	<key>NFCReaderUsageDescription</key>
	<string>Read My Number Card.</string>
	<key>com.apple.developer.nfc.readersession.felica.systemcodes</key>
	<array>
		<string>12fc</string>
	</array>
	<key>com.apple.developer.nfc.readersession.iso7816.select-identifiers</key>
	<array>
		<string>D392F000260100000001</string>
		<string>D3921000310001010401</string>
		<string>D3921000310001010402</string>
		<string>D3921000310001010408</string>
	</array>
```

マイナンバーカードはNFC TypeBなのでFelicaは関係ないと思ったのですが、Felicaも指定しないとなぜかカード読み込みのシステムダイアログが表示されませんでした。

## Android/iOS共通のコード

Android/iOS共通のコードとしては、`NfcManager.instance.isAvailable()`でNFCが利用可能か調べて、`NfcManager.instance.startSession()`でカードの検出を行います。

```dart
    // NFC通信を行う
    bool isAvailable = await NfcManager.instance.isAvailable();
    if (!isAvailable) {
      _setMessage('NFCが使えません');
      return;
    }
    _setMessage('カードをタッチしてください');
    try {
      await NfcManager.instance.startSession(
        alertMessage: "カードにタッチしてください",
        onDiscovered: (NfcTag tag) async {
          try {
            if (Platform.isIOS) {
              await onDiscoveredForIos(tag);
            }
            if (Platform.isAndroid) {
              await onDiscoveredForAndroid(tag);
            }
          } catch (e) {
            _setMessage('Error\n${e.toString()}');
            NfcManager.instance.stopSession(errorMessage: 'Error ${e.toString()}');
          }
          NfcManager.instance.stopSession();
        },
      );
    } catch (e) {
      _setMessage('Error\n${e.toString()}');
    }
```

## Android固有のコード
マイナンバーカードはNFC TypeB形式のカードですが、ISO7816-4コマンドで通信するため`NfcB`ではなく`IsoDep`を使用します。

```dart
    final isoDep = IsoDep.from(tag);
    if (isoDep == null) {
      _setMessage('未対応のカードです');
      return;
    }
```

IsoDepの場合、APDUプロトコルのトレーラー部も含めて返却されるため、受信データの末尾2バイトがトレーラー部で、末尾の1バイトで成否判定を行います。

```dart
      // READ BINARY: マイナンバー読み取り
      cmd = [0x00, 0xB0, 0x00, 0x00, 0x00];
      res = await isoDep.transceive(data: Uint8List.fromList(cmd));
      if (res.last != 0x00) return;

      s = 'マイナンバー：';
      s += utf8.decode(res.sublist(3, 15));
```

なお動作確認はシミュレータではNFCが使えないため実機が必要になります。
サンプルとしてマイナンバーと基本4情報を読み込むようにしてみました。

![Screenshot_20231217-182453.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19193/34fae25f-65cc-2ceb-a493-01c425a40545.png)


思いっきり個人情報なので画像にモザイクかけますが、きちんと読み込むことができました。

## iOS固有のコード

iOSではISO7816そのままの`Iso7816`を使用します。

```dart
    final iso7816 = Iso7816.from(tag);
    if (iso7816 == null) {
      _setMessage('未対応のカードです');
      return;
    }
```

`Iso7816`では受信データはペイロード部（`res.payload`）とトレーラー部（`res.statusWord1`と`res.statusWord2`）に分かれていますが、アルゴリズムはAndroidと変わりません。

```dart
      // READ BINARY: マイナンバー読み取り
      cmd = [0x00, 0xB0, 0x00, 0x00, 0x00];
      res = await iso7816.sendCommandRaw(Uint8List.fromList(cmd));
      if (res.statusWord2 != 0x00) return;

      s = 'マイナンバー：';
      s += utf8.decode(res.payload.sublist(3, 15));
```

iOSもシミュレータでは動作確認できないため実機で確認しましょう。

![IMG_0001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19193/f492111f-c5dc-d3b1-8340-cdb4e931c9bb.png)


こちらもちゃんと読み込めてますね。

ソースコード全体はこちらです。

https://github.com/tanapi/flutter_my_number

## おわりに

友人と鳥メロで飲んでたわけですよ。
そこで、「天才的なアプリの企画を思いついたのよ！」とこだわり酒場のレモンサワーのジョッキを空けながら、その友人にマイナンバー素数マッチングアプリの企画を熱弁したのです。
あのザッカーバーグだってマッチングアプリとしてFacebook立ち上げたんだぞと。

友人は卓上の炙る!!〆さばを食べながら、こう言うのです。
「オレだったら得体のしれないアプリにマイナンバーみたいなセンシティブな個人情報を預けたくないし。うん、**無理。絶対無理**」

こうして日本のザッカーバーグは産まれることなく、不要となったソースコードをここで供養したのでした。

素数だけに「**割り切れない恋が、ここにはある**」ってアプリのキャッチコピーも合わせて供養しときます。