モバイルFeliCaとの通信について





ここでは、モバイルFeliCaとコントロールプログラム間の通信について説明する。




モバイルFeliCaについて

モバイルFeliCaとは、FeliCaチップを携帯電話に搭載することで携帯電話をFeliCaカードのように扱えるようにしたものである。
FeliCaチップ内部のメモリ領域は、外部リーダ/ライタだけでなく、携帯アプリ側からも参照することができる。
モバイルFeliCaを使うためには、フリー領域や共通領域を使用することを宣言した携帯アプリが事前にインストールされている必要がある。
また、共通領域を使うためにはFeliCa Networks社に登録申請(有料高額)を行う必要がある。
逆に、フリー領域は自由に使うことができるが、1つのアプリに対し64バイトしか領域が割り当てられず、その対応は下図のようになる。

モバイルFeliCaとメモリ領域
同じアプリでも3つのうちのどの領域が割り振られるかはインストールされる携帯電話によって変化する。

モバイルFeliCaとの通信は、通常のFeliCaカードとは異なり、認証のために下図のチャートに従った手順を踏む必要がある。
しかしこれらの認証手順はさして難しいものではなく、基本的に前項で説明したFeliCaのAPIを使うことで実現することができる。
また、この認証は総当たり法で解除することができるため、フリー領域に個人情報や金融関係の情報を長時間書き込んでおくのは避けた方が良い。
外部リーダ/ライタから携帯アプリを起動する行為のみ、唯一通常には用いないAPIを利用する。

モバイルFeliCa通信手順例


モバイルFeliCaのポーリング

モバイルFeliCaをポーリングするのに変わった手段は用いない。
システムコードを{0xFF,0xFF}にしてもモバイルFeliCaは検出できるが、専用の{0x80,0xCD}を用いた方が排他的で確実である。



フリー領域の読み込みと書き換え

モバイルFeliCaのフリー領域を書き換える前に、モバイルFeliCaチップとの認証を行う必要がある。
認証の手順は、フリー領域の中から対象アプリに該当する領域を探すこと、そしてパスワードの照合である。
前者はCPIDコード照合、後者はPINコード解除と呼ばれる。
CPIDとPINの値は、アプリを作成する時に開発者によって定められる値である。

モバイルFeliCaのフリー領域を宣言したアプリは、3つあるフリー領域の中の一つを1対1対応で割り当てられる構造になっている。
フリー領域を使うことを宣言していないアプリはフリー領域を参照することもできず、また1つのアプリに2つ以上のフリー領域を割り当てることもできない。
さらに、面倒くさいことだが、コントロールプログラム側からでは目的のアプリに割り振られた領域を一目で認識することができない。
つまりCPIDコード照合とは、3つあるフリー領域の中から目的のアプリに対応したフリー領域を順に検索することである。
検索の方法は、コントロールプログラム側で用意した16バイトのCPIDと、モバイルFeliCa領域内部のCPIDとが完全一致するかを確かめることで行われる。
まず、フリー領域の特定番地に格納されているCPIDをreadBlockWithoutEncryptionコマンドで読み出す。
もし読み出したCPID値とアプリ作成時に開発者が定めたCPID値が一致した場合、そのフリー領域は目的のアプリと対応した領域と見なすことができる。

PINコードはPersonal Identification Numberの略称であり、4バイトの数字で表わされる。
PINコードのセキュリティを解除するには、writeBlockWithoutEncryptionコマンドを用いる。
CPID照合で一致したフリー領域のPINコード部に、元の情報と同じデータをwriteBlockで書き込むと、PINコードが解除される。

以上2つの認証手順を行った後に、初めてモバイルFeliCaフリー領域のアプリケーションデータブロック部にアクセスすることができる。
データ部を書き換える方法は通常のread/writeと同じで、システムコードをモバイルFeliCaのものに変えれば良い。

認証時にreadBlockWithoutEncryptionとwriteBlockWithoutEncryptionでアクセスするサービスコード値は、以下の図を参照すること。

フリー領域の構成図

以下に、CPID照合・PIN解除・データ書き換えのコード例を記す。

/*
 * 引数で指定されたIDmに対して、CPIDとマッチングを行う
 * @param1 : cardIDm, カードのIDm
 * @param2 : CPID, CPIDコード
 * @return : 0~2)フリー領域番号 3)マッチング失敗。 -1)領域の読み込み失敗
 */
int matchingCPID( unsigned char* cardIDm, unsigned char* CPID ) {
	//-- parameter initialization --
	unsigned char serviceCodeList[2];
	unsigned char blockList[2] = {0x80, 0x00};
	int numOfBlocks   = 1; // block num
	int numOfServices = 1; // service num
	// inputReadBlock.services_code_list is configured in roop each time
	//-- output parameter --
	unsigned char readBlockData[16];

	//-- CPID matching --
	int i;
	for( i=0; i<3; i++ ) {
		// configure service code list each roop counter.
		serviceCodeList[0] = 0x09;
		serviceCodeList[1] = 0x10 + (i*0x10);

		// non encryption reading
		if( !readDataWithoutEncryption(cardIDm, serviceCodeList, numOfServices,
			blockList, numOfBlocks, readBlockData) ) {
			error_routine();
			return -1;
		}
		// if success to read, check the CPID
		int j;
		for( j=0; j<16; j++ ) {
			if( readBlockData[j] != CPID[j] ) {
				break;
			}
		}
		// CPID is correct
		if( j==16 ) {
			break; // for(i)を抜ける
		}
	} // for(i)
	return i;
} // matchingCPID()


/*
 * 引数で指定されたPINとマッチングを行う
 * @param1 : cardIDm, カードのIDm
 * @param2 : index, matchingCPID()の引数が入る
 * @param3 : PIN, PINコード
 * @return : 成功 or 失敗のT/F
 */
bool matchingPIN( unsigned char* cardIDm, unsigned char* PIN, int index ) {
	//--- parameter initialization ---
	int numOfServices = 1;
	unsigned char serviceCodeList[2] = { 0x29, 0x11+(index*0x10) };
	int numOfBlocks = 1;
	unsigned char blockList[2] = { 0x80, 0x00 };

	bool res;
	res = writeDataWithoutEncryption( cardIDm, serviceCodeList, numOfServices, blockList, numOfBlocks, PIN );
	return res;
}; // matchingPIN()


/* 
 * @param1 : cardIDm, カードの固有IDm
 * @param2 : blockList, 書き込み先のブロックリスト指定
 * @param3 : numOfBlocks, 書き込み先のブロック数
 * @param4 : blockData, 書き込みブロック
 * @param5 : index, 何番目のフリー領域を指定するか
 */
bool mfelicaWriteBlock( unsigned char* cardIDm, unsigned char* blockList, int numOfBlocks, unsigned char* blockData, int index ) {
	// サービスコードリスト
	unsigned char serviceCodeList[2] = { 0x09, (0x11+(index*0x10)) };
	int numOfServices = 1;

	return writeDataWithoutEncryption( cardIDm, serviceCodeList, numOfServices,
						blockList, numOfBlocks, blockData );
}

/*
 * @param1 : cardIDm, カードの固有IDm
 * @param2 : blockList, 読み込み先のブロックリストを指定
 * @param3 : numOfBlock, 読み込み先のブロック数の指定
 * @param4 : blockData, 読み込んだデータの格納先
 * @param5 : index, 何番目のフリー領域を指定するか
 */
bool mfelicaReadBlock(unsigned char* cardIDm, unsigned char* blockList, int numOfBlocks, unsigned char* blockData, int index ) {
	//--- 読み込み時の設定 ---
	unsigned char serviceCodeList[2] = { 0x09, (0x11+(index*0x10)) };
	int numOfServices = 1;

	return readDataWithoutEncryption( cardIDm, serviceCodeList, numOfServices,
						blockList, numOfBlocks, blockData );
}




外部リーダ/ライタからiアプリを起動する

外部リーダ/ライタから、つまりコントロールプログラムから任意のiアプリを起動するには、iアプリ起動コマンドを送信する。
コマンドの概要はSDK for FeliCaに載っているが、そのコマンドで送るべき起動情報はSDK for FeliCaには載っていない。
書籍にこのコマンドで送るべき起動情報が詳細が載っている。

iアプリ起動コマンドを送る際には、アプリ起動時の初期変数として、140〜160バイト程度のデータを与えることができる。
この初期変数のデータサイズはコマンドの長さに関係を持つ。
コマンドの最大長は一定であり、コマンドの起動情報はアプリダウンロード元のURLを内包する。
つまりiアプリを設置するサーバーのURLが短ければ短いほど多くのデータを初期変数として与えることができる。

また、FeliCa OS Ver. 2.0の場合、iアプリ起動方法が一部簡略化されている。