Go製 Felica ライブラリ pasori に MAC付きRead とMAC付きWriteを足した

前回のブログ の続きです。前回はMACをつけないRead/Writeのコマンドを足しましたが、今回はMAC付きのRead/Writeのコマンドを足しました。前回から10日間ずっと、この実装のバグが取れずに悩んでたのでようやくブログにできる程度に実装できたのは素直にうれしいですね。

MAC といっても MAC(Media Control address)アドレスの MACではなく、Message Authentication Code の略で、データが改ざんされていないかをチェックするためのコードのことです。

参考資料

SONYのFelica技術情報 から 『FeliCa Lite-Sユーザーズマニュアル』のPDFを参照。(なぜか直接リンクだと表示されないので、いったん技術情報のページに繊維してから「ダウンロード」を選択すること)

FeliCa - おなかすいたWiki!

[PASMO] FeliCa から情報を吸い出してみる - FeliCaの仕様編 [Android][Kotlin] - Qiita

FeliCa Liteの片側認証 - hiro99ma site

使い方

MAC付き読み出しは2種類あります。MACブロックを使う方法とMAC_Aブロックを使う方法です。MACブロックはFelica Lite からの互換性のために存在していて、Felica Lite-S ではMAC_Aブロックを使うことが推奨されています。MACブロックを使ってMAC付き書き込みはできないので、MAC付き書き込みを行う場合はMAC_Aブロックを使います。

注意点として、MAC付き読み出し/書き込みを行う前に、事前にカード鍵ブロックにカード鍵を書き込んでおく必要があります。また、カード接触後にランダムチャレンジブロックにランダムな値を事前に書き込んでおく必要があります。

MACブロックを使ってのMAC付読み出し

基本的な使い方は cmd/with_mac に例があるのでそれを掲載します

package main

import (
    "fmt"

    "crypto/rand"

    "github.com/bamchoh/pasori"
)

func main() {
    var err error
    fmt.Println("Please touch FeliCa")

    // pasori 初期化
    psr, err := pasori.InitPasori()
    if err != nil {
        panic(err)
    }
    defer psr.Release()

    // ランダムチャレンジデータ生成
    psr.RC = make([]byte, 16)
    _, err = rand.Read(psr.RC)
    if err != nil {
        panic(err)
    }

    // ランダムチャレンジブロックに書き込み
    err = psr.FelicaWriteWithoutEncryption(pasori.RC, psr.RC)
    if err != nil {
        panic(err)
    }

    // カード鍵生成
    // [注意]
    // カード鍵は悪意のある第三者にバレると改ざんされてしまうので
    // 本来はソースコードに記述するのではなく、外部ファイル等で
    // 管理しておくことをお勧めします。
    //
    // あと、カードにいったん同じ鍵を書き込んでおくこと。そうしないと
    // MAC生成時にエラーになります。
    // 書き込みは普通に WriteWithoutEncryption で書き込めばOK
    psr.CK = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}

    // S_PAD0 ブロックに適当に書き込み
    err = psr.FelicaWriteWithoutEncryption(pasori.S_PAD0, []byte{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3})
    if err != nil {
        panic(err)
    }

    // MAC付き読み出し
    // 第一引数にはサービスコード、第二引数以降には読み出し先ブロック番号を指定
    blocks, err := psr.FelicaReadWithMAC(pasori.SERVICE_RO, pasori.S_PAD0)
    if err != nil {
        panic(err)
    }
    fmt.Println("s_pad0", blocks[0])
}

MAC_Aブロックを使ってのMAC付読み出し/書き込み

基本的な使い方は cmd/with_mac_a に例があるのでそれを掲載します

package main

import (
    "fmt"

    "crypto/rand"

    "github.com/bamchoh/pasori"
)

func main() {
    var err error
    fmt.Println("Please touch FeliCa")

    // pasori 初期化
    psr, err := pasori.InitPasori()
    if err != nil {
        panic(err)
    }
    defer psr.Release()

    // ランダムチャレンジデータ生成
    psr.RC = make([]byte, 16)
    _, err = rand.Read(psr.RC)
    if err != nil {
        panic(err)
    }

    // ランダムチャレンジブロックに書き込み
    err = psr.FelicaWriteWithoutEncryption(pasori.RC, psr.RC)
    if err != nil {
        panic(err)
    }

    // カード鍵生成
    // [注意]
    // カード鍵は悪意のある第三者にバレると改ざんされてしまうので
    // 本来はソースコードに記述するのではなく、外部ファイル等で
    // 管理しておくことをお勧めします。
    psr.CK = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}

    // MAC付き書き込み
    // 第一引数には書き込み先ブロック番号、第二引数には書き込みデータ
    err = psr.FelicaWriteWithMAC_A(pasori.S_PAD0, []byte{3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3})
    if err != nil {
        panic(err)
    }

    // MAC付き読み出し
    // 第一引数にはサービスコード、第二引数以降には読み出し先ブロック番号を指定
    blocks, err := psr.FelicaReadWithMAC_A(pasori.SERVICE_RO, pasori.S_PAD0, pasori.S_PAD1)
    if err != nil {
        panic(err)
    }
    fmt.Println("s_pad0", blocks[0])
    fmt.Println("s_pad1", blocks[1])
}

MACの計算のハマりポイント

felica のDES暗号をGoで表現するには?

FelicaMAC生成には 2-key Triple DES-CBC が使われています。 DES暗号はGoではdes パッケージで取り扱うことができます。ちょうど Triple DES もパッケージの中にあるので、それを使いました。des.NewTripleDESCiper で初期化しますが、引数の key に渡すバイト配列は 24バイトでなければならず、はじめ、なにを設定すればいいのかわかりませんでした。ただ、『Felica Lite-S ユーザーズマニュアル』には セッション鍵であれば、Key1, Key2, Key3 として CK1[0]-[7], CK2[0]-[7], CK1[0]-[7] と記載があります。それを連結させた値を des パッケージの key に渡すのだと思い当たるのにそれほど時間は要りませんでした。ただ、単純に24バイトの連結した値を指定すればいいのではなくバイト単位で配列を反転させて設定してやる必要がありました。(これは Felica 側の仕様で、Triple DES の仕様ではないようです) つまり、指定する24バイトの配列は、セッション鍵生成時のTriple DESであれば、CK1[7]-[0], CK2[7]-[0], CK1[7]-[0] を連結させた24バイトを指定しなければなりませんでした。

MAC付き読み出し(MACブロック)、MAC付き読み出し(MAC_Aブロック)、MAC付き書き込みで入力パラメータが微妙に違う

全ての方法において、2-key Triple DES-CBC を使用して MACを算出するというのは同じなんですが、KEYに指定するパラメータが違ったり、初期値として代入するデータが違ったりします。(当たり前か)

メソッド key1 key2 key3 入力値
セッション鍵の生成 CK1 CK2 CK1 RC1
MAC付き読み出し(MACブロック) SK1 SK2 SK1 RC1 と ブロックデータ1[0]-[7] との排他的論理和
MAC付き読み出し(MAC_Aブロック) SK1 SK2 SK1 RC1 と ブロック番号との排他的論理和
MAC付き書き込み(MAC_Aブロック) SK2 SK1 SK2 RC1 と 複合ブロック(※1)との排他的論理和

(※1) WCNT と 書き込みブロック番号を8バイトに詰め込んで1ブロックとしたもの

読み出しはMACブロックが値として取得できるので、計算結果との検算がしやすいのだが、書き込みの場合は検算ができないので、正しく書き込めたかどうかぐらいしか情報がなく、デバッグにどえらい時間がかかってしまった。また、KEYが書き込みの時だけ逆だったり、WCNTの値を取得してこなければならなかったりと、書き込みだけ複雑な構造をしているので余計にハマりやすかった。

メモリコンフィギュレーションブロックにMAC付きアクセスの設定をしておかないとエラーになる

メモリコンフィギュレーションブロック(MC) というブロックがあり、各ブロックへのアクセス権限を設定できるようになっている。初期状態では、MAC付きアクセスなしの状態なので、MAC付きアクセスありの状態にしてやる必要がある。また、MAC付きアクセスありの設定にすると元に戻すことはできないらしいので注意が必要。どのビットがどのブロックに対応しているかは『Felica Lite-S ユーザーズマニュアル』を参照のこと。