はじめに
タイトルのような環境で,APDUを用いたHCEとの通信を行いたかったのでメモ.
Raspberry Piとは
美味しいお菓子.
https://www.raspberrypi.org/
PaSoRiとは
PCでNFCが扱えるやべーやつ.
https://www.sony.co.jp/Products/felica/consumer/products/RC-S380.html
Androidとは
iOSじゃないやつ.
https://www.android.com/
HCEとは
Host Card Emulationの略で,Android端末をNFCタグとして扱う方法.
APDUという規格を使って,自由なデータ通信もできる.
今回はHCE-F(FeliCa準拠)じゃないほうを使う.
https://developer.android.com/guide/topics/connectivity/nfc/hce
環境
- Raspberry Pi 3 Model B+
- Arch Linux ARMv7 (Kernel 4.14.90-1-ARCH)
- SONY PaSoRi RC-S380
- ASUS Zenfone 4 Z01KDA/ZE554KL
- Android 8.0.0
準備
Pythonのインストール
nfcpyはPython 3系に対応していないので,2系を使う.
v1.0.0 で Python 3 系に対応しました.
# pacman -Syu python python-pip
nfcpyのインストール
# pip install nfcpy
テスト
nfcpyのGitリポジトリからサンプルを入手して実行する.
# pacman -Syu git
$ git clone https://github.com/nfcpy/nfcpy
$ cd nfcpy/examples
$ python tagtool.py
No handlers could be found for logger "nfc.llcp.sec"
[nfc.clf] searching for reader on path usb
[nfc.clf] no reader available on path usb
[main] no contactless reader found on usb
[main] no contactless reader available
リーダが見つからない,と言われるので,lsusbで確認してみる.
$ lsusb
Bus 001 Device 004: ID 054c:06c3 Sony Corp. RC-S380
Bus 001 Device 003: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 002: ID 0424:2514 Standard Microsystems Corp. USB 2.0 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
しっかり認識されている.
では,デバイスIDを指定して実行してみる.
$ python tagtool.py --device usb:054c:06c3
No handlers could be found for logger "nfc.llcp.sec"
[nfc.clf] searching for reader on path usb:054c:06c3
[main] access denied for device with path usb:054c:06c3
[main] no contactless reader available
今度はアクセスが拒否された.
デバイスIDを指定しないと,アクセスが拒否されたリーダは無視するようだ.
udevのルールを用いて,plugdevグループのユーザにリーダの使用を許可する.
# echo SUBSYSTEM==\"usb\", ACTION==\"add\", ATTRS{idVendor}==\"054c\", ATTRS{idProduct}==\"06c3\", GROUP=\"plugdev\" >> /etc/udev/rules.d/nfcdev.rules
リーダを使うユーザがplugdevグループに参加していなければならないので,確認する.
$ groups
wheel siketyan
グループがない,もしくは参加していなければ,作成および参加を行う.
# groupadd plugdev
# usermod -aG plugdev siketyan
接続しなおすか,再起動して再確認.
$ python tagtool.py --device usb:054c:06c3
No handlers could be found for logger "nfc.llcp.sec"
[nfc.clf] searching for reader on path usb:054c:06c3
[main] the reader on usb:054c:06c3 is busy
[main] no contactless reader available
今度は,リーダがビジーだと言われた.
カーネルモジュールのport100と競合しているようなので,無効化して再確認.
# rmmod port100
$ python tagtool.py
No handlers could be found for logger "nfc.llcp.sec"
[nfc.clf] searching for reader on path usb
[nfc.clf] using SONY RC-S380/P NFC Port-100 v1.11 at usb:001:005
** waiting for a tag **
タグ待ちに入った!
Android端末をタッチしてみる.
Type4ATag MIU=255 FWT=0.038664
ちゃんと認識したみたい!
攻めと受け
NFCにおいてどちらが攻めでどちらが受けかという問題は,けっこう難しい.
今回の場合,先に電波を出してタグを探すのはPaSoRi側なので,こちらが攻めかもしれない.
ただ,物理的にはタグを近づけていくのでこちらが攻めかもしれない.
ここでは,PaSoRi側を攻め,タグ側を受けと仮定して話を進める.
Android HCEで受け側をつくる
Google信者の方はAndroid Studio,JetBrains信者の方はIntelliJ IDEAで,新規Androidプロジェクトを作成する.
Android SDKのセットアップは情報が多いと思うので適当に.
まずはManifestを書く.
今回はNFCによってバイブレータ動作させたいので,VIBRATE権限を要求.
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.sikeserver.nfc.test">
<uses-feature
android:name="android.hardware.nfc.hce"
android:required="true"
tools:targetApi="eclair" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.VIBRATE"/>
<application android:allowBackup="true"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:targetApi="donut">
<service
android:name=".service.ApduService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/apduservice" />
</service>
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<host-apdu-service
xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/app_name"
android:requireDeviceUnlock="false">
<aid-group
android:description="@string/app_name"
android:category="other">
<aid-filter android:name="F1145141919810" />
</aid-group>
</host-apdu-service>
Manifestで指定したサービスをJavaで書く.
package com.sikeserver.nfc.test.service
import android.nfc.cardemulation.HostApduService;
import android.os.Bundle;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Log;
import java.util.Arrays;
public class ApduService extends HostApduService {
private static final byte[] COMMAND_SELECT = new byte[] {
(byte) 0x00, // CLA
(byte) 0xA4, // INS
(byte) 0x04, // P1
(byte) 0x00, // P2
(byte) 0x07,
// AID
(byte) 0xF1,
(byte) 0x14,
(byte) 0x51,
(byte) 0x41,
(byte) 0x91,
(byte) 0x98,
(byte) 0x10
};
private static final byte[] COMMAND_VIBRATE = new byte[] {
(byte) 0x00, // CLA
(byte) 0x11, // INS
(byte) 0x45, // P1
(byte) 0x14 // P2
};
private static final byte[] RESPONSE_OK = new byte[] {
(byte) 0x90, // SW1
(byte) 0x00 // SW2
};
@Override
public byte[] processCommandApdu(byte[] command, Bundle extras) {
if (Arrays.equals(command, COMMAND_SELECT)) {
Log.d("NFCTest", "AID Selected");
} else if (Arrays.equals(command, COMMAND_VIBRATE)) {
Log.d("NFCTest", "Vibrate");
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
if (vibrator != null) {
vibrator.vibrate(
VibrationEffect.createOneShot(
100,
VibrationEffect.DEFAULT_AMPLITUDE
)
);
}
}
return RESPONSE_OK;
}
}
Pythonで攻め側をつくる
がーって書く.
詳しくはnfcpyのドキュメントを参照のこと.
AIDとバイブレートコマンドは適宜書き換えて.
import nfc
def select(tag):
result = tag.send_apdu(
0x00, # CLA
0xA4, # INS
0x04, # P1
0x00, # P2
bytearray.fromhex('F1145141919810'), # AID
check_status=False
)
return True
def vibrate(tag):
result = tag.send_apdu(
0x00, # CLA
0x11, # INS
0x45, # P1
0x14, # P2
check_status=False
)
return True
def startup(targets):
return targets
def connected(tag):
print("Tag Connected")
select(tag)
vibrate(tag)
return True
def released(tag):
print('Tag Released')
def main():
clf = nfc.ContactlessFrontend('usb')
print(clf)
if clf:
while clf.connect(rdwr={
'on-startup': startup,
'on-connect': connected,
'on-release': released,
}):
pass
if __name__ == '__main__':
main()
まとめ
ピッってできるとたのしい!
参考
- Python module for near field communication - nfcpy 0.13.5 documentation
https://nfcpy.readthedocs.io/en/latest/ - カーネルモジュール - ArchWiki
https://wiki.archlinux.jp/index.php/カーネルモジュール - VibrationEffect | Android Developers
https://developer.android.com/reference/android/os/VibrationEffect - python - How to use the send_apdu() command in nfcpy library? - StackOverflow
https://stackoverflow.com/questions/42007592/how-to-use-the-send-apdu-command-in-nfcpy-library - コマンドとレスポンス
http://eternalwindows.jp/security/scard/scard07.html