19
16

More than 1 year has passed since last update.

IDmでの認証は危険なのでnfcpyでFeliCa Lite-Sの内部認証を実装する

Last updated at Posted at 2021-03-19

FeliCaで電子錠、してますか?実装方法によってはそれ、危険かもしれません

この記事を書こうと思ったきっかけ

FeliCaで家の鍵を解除するようなプログラムを書いている人の中でIDmを使って認証をしている人がいますが、それは非常に危険です。
IDmはPollingというコマンドで簡単に入手することができるし、IDmは簡単に偽造することができます。
(IDmが簡単に取得できるからこそIDmでこういうことをやっている人が多いとも言える)
IDmはユニークなIDなのでICカードの特定に使うことはできますが、認証に使うことはできません。
そこで使われるのが内部認証というものなのですが、Googleで調べてもIDm取得して終わり!!!というものばかりでほとんど情報が出てこなかったので今回自分で書きました。
この記事のわからないところがあればコメント欄か私のTwitter(かDiscord)で聞いてください。

実装

必要なもの

FeliCa Lite-Sカード
NFCカードリーダー/ライター
Python
nfcpy
パソコン
ユーザーズマニュアルの20ページを印刷したもの(必須ではないがあると便利)

よく使う基本のコード

これはよく使うので覚えておいてください。
また、末尾がhになっている数字は16進数、bになっている数字は2進数です。

定義。ファイルの一番最初に書く
import nfc
clf = nfc.ContactlessFrontend("usb")
tag = clf.connect(rdwr={'on-connect': lambda tag: False})
sc_read = nfc.tag.tt3.ServiceCode(0, 0x0b) # データを読み込むときに使います
sc_write = nfc.tag.tt3.ServiceCode(0, 0x09) # データを書き込むときに使います
bc00 = nfc.tag.tt3.BlockCode(0x00, service=0) # 第一引数はブロック番号(ユーザーズマニュアルの表3-1の一番左)。serviceは一つしかないので0
bc01 = nfc.tag.tt3.BlockCode(0x01, service=0)
ret = tag.read_without_encryption([sc_read], [bc00, bc01]) # S_PAD0とS_PAD1(bc00とbc01のブロックの名称)のデータを読み出す
tag.write_without_encryption([sc_write], [bc00], b"meow!meow!meow!!") # S_PAD0に「meow!meow!meow!!」と書き込む(16bytes)
# Write Without Encryptionコマンドは本当はレスポンスがあるのですがnfcpyでは返り値がありません...

1次発行

1次発行で内部認証に必要な情報をFeliCa Lite-Sに書き込みます

IDの書き込み

IDとIDmは別物で、IDdとIDmは同じものです。
IDは16バイトで構成されていて、上位8バイトにIDdが自動で格納され、その後の2バイトにDFC(使用しないならALL_00h)、下位6バイトに自由な値を書き込みできます。
この3つすべてを合わせてここではIDといいます。
必ずカードごとに異なるIDをつけてください
("\x00"*10 + os.urandom(6)など)

つまり、\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xXX\xXX\xXX\xXX\xXX\xXX になります。(XXは任意の1バイト)
これを、ブロック番号82hのIDに書き込みます。

なお、ここで上位8バイトもALL_00hにしているのは、書き込みをして電源を切断したあとに自動でIDdになるからです。
電源切断後read_without_encryptionで読み出すとIDdになっていることが確認できます。

IDの書き込み
ID = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xbf\x05\xce\x9e\xe4n" # この値はサンプルです
bc82 = nfc.tag.tt3.BlockCode(0x82, service=0)
tag.write_without_encryption([sc_write], [bc82], ID)

カード鍵の作成

カードが複数になるとその分カード鍵を作成し、保存しなければなりません。(理論上は全て同じでも構わないが、危険)
しかし、マスターキーを作成しそこからカード鍵を生成することによって実質的に管理するものはマスターキーのみで済みます。
FeliCa Lite-S 個別化カード鍵標準生成アルゴリズム を見ながら実装します(見ないと多分できません)

IDブロックは電源切断後上位10バイトがIDd(IDm)に自動でなるので、IDの格納から電源を切断せずに個別化カード鍵を生成するときも上位10ビットをIDd(IDm)にする必要があります。

電源切断前ならこっち
print(tag.idm + ID[-6:])

電源切断後に生成する場合はread_without_encryption関数でIDブロックの中身を取り出しても構いません。

電源切断後ならこちらでも
bc82 = nfc.tag.tt3.BlockCode(0x82, service=0)
print(tag.read_without_encryption([sc_read], [bc82]))
個別化カード鍵生成プログラム
import os
import secrets
import Crypto.Cipher.DES

M = b"" # tag.idm + ID[-6:], もしくはread_without_encryptionでprintされたもの

key = b'' # この鍵はマスター鍵です。secrets.token_bytes(24)などで作ってください。運用時もずーっと使うので大切に保管しておいてください

zero = "\x00" * 8
KA = key[:8] # 8ビットずつに分けて
KB = key[8:16]
KC = key[16:]
Ka = Crypto.Cipher.DES.new(KA, Crypto.Cipher.DES.MODE_ECB) # 鍵の定義
Kb = Crypto.Cipher.DES.new(KB, Crypto.Cipher.DES.MODE_ECB)
Kc = Crypto.Cipher.DES.new(KC, Crypto.Cipher.DES.MODE_ECB)

def triple_DES(a, b, c, plain):
    return c.encrypt(b.decrypt(a.encrypt(plain))) # plainをaで暗号化してbで復号化してcで暗号化する

L = triple_DES(Ka, Kb, Kc, zero)
L = int.from_bytes(L, byteorder="big")

if len(bin(L)) == 66: #最上位ビットが1
    K1 = L << 1
    K1 = K1 & 0xFFFFFFFFFFFFFFFF # 0b1111111111111111111111111111111111111111111111111111111111111111
    K1 = K1 ^ 0x1B
else:
    K1 = L << 1
    K1 = K1 & 9223372036854775807 # 0b111111111111111111111111111111111111111111111111111111111111111

M1  = M[:8]
M2_ = int.from_bytes(M[8:], byteorder="big")
M2 = K1 ^ M2_
C1 = triple_DES(Ka, Kb, Kc, M1)
T = triple_DES(Ka, Kb, Kc, (int.from_bytes(C1, byteorder="big") ^ M2).to_bytes(8,  byteorder="big"))
M1_ = (int.from_bytes(M1, byteorder="big") ^ 0x8000000000000000).to_bytes(8,  byteorder="big")
C1_ = triple_DES(Ka, Kb, Kc, M1_)
T_ = triple_DES(Ka, Kb, Kc, (int.from_bytes(C1_, byteorder="big") ^ M2).to_bytes(8,  byteorder="big"))

C = T + T_
print(C)

ここで出力されたもの(変数Cの中身)が個別化カード鍵です。
これをブロック番号87h、「CK」(おそらくCard Key)に書き込みます。

CK = b'@C\xa863\xcf!\n1\xe8\xe0H\xd4\x89\xa6\x04' # 個別化カード鍵。サンプルです
bc87 = nfc.tag.tt3.BlockCode(0x87, service=0)
tag.write_without_encryption([sc_write], [bc87], CK)

カード鍵バージョンの書き込み

カード鍵バージョンは16バイト分ありますが、実際に使えるのは上位2バイトです。
ブロック番号86h、「CKV」(Card Key Version)に書き込みます

CKV = "\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" # 上位2バイトの「\x00\x01」部分のみ任意の値にできる
bc86 = nfc.tag.tt3.BlockCode(0x86, service=0)
tag.write_without_encryption([sc_write], [bc86], CKV)

ユーザーブロックの書き込み及び2次発行はやらなくても構いません。(内部認証に関係ない)

内部認証をする

流れの説明

image.png
こんな感じで認証をします。
ここで使われているMAC_Aは
image.png
image.png
こんな感じで生成します(理解に時間がかかった...)
☆印のところでバイトオーダーを反転させます。
また、⊕は排他的論理和です。

れっつコーディング

全部一つにまとめるとわかりにくくなるのでいろいろ分割します

まず、チャレンジブロック(80h)に乱数RCを書き込んでID、CKV、MAC_Aを読み出します

import secrets
RC = secrets.token_bytes(16) # 16バイトの乱数。認証でもう一度使うので別のプログラムで実行するならprintするなどして保管してください。
bc80 = nfc.tag.tt3.BlockCode(0x80, service=0)
tag.write_without_encryption([sc_write], [bc80], RC)

bc82 = nfc.tag.tt3.BlockCode(0x82, service=0) # ID
bc86 = nfc.tag.tt3.BlockCode(0x86, service=0) # CKV
bc91 = nfc.tag.tt3.BlockCode(0x91, service=0) # MAC_A

ret = tag.read_without_encryption([sc_read], [bc82, bc86, bc91])
ID = ret[:16] # RCと同様に保管してください
CKV = ret[16:32] #同上
MAC_A_Card = ret[32:40] # 最後にあっているかどうか確認するために必要です。
セッション鍵生成コード
RC = b'' # さっきの16バイトの乱数。同じプログラムで実行するならこの行は不要です
RC1 = RC[:8]
RC2 = RC[8:]

CK = b'' # カード鍵(=個別化カード鍵)。実際に運用するときには上にあげたコードで個別化マスター鍵から生成することになると思います。
CK1 = CK[:8]
CK2 = CK[8:]


RC1 = int.from_bytes(RC1, byteorder="big").to_bytes(8, byteorder="little")
CK1 = Crypto.Cipher.DES.new(int.from_bytes(CK1, byteorder="big").to_bytes(8, byteorder="little"), Crypto.Cipher.DES.MODE_ECB)
CK2 = Crypto.Cipher.DES.new(int.from_bytes(CK2, byteorder="big").to_bytes(8, byteorder="little"), Crypto.Cipher.DES.MODE_ECB)

SK1_ = CK1.encrypt(CK2.decrypt(CK1.encrypt(RC1)))
SK1 = int.from_bytes(SK1_, byteorder="big").to_bytes(8, byteorder="little") # セッション鍵1です。RCと同様に(ry
RC2 = int.from_bytes(RC2, byteorder="little")
RC2_ = RC2 ^ int.from_bytes(SK1_, byteorder="big")

SK2_ = CK1.encrypt(CK2.decrypt(CK1.encrypt(RC2_.to_bytes(8, byteorder="big"))))
SK2 = int.from_bytes(SK2_, byteorder="big").to_bytes(8, byteorder="little") # セッション鍵2です。RCと同様に(ry
MAC_A生成コード
ID = b"" # さっきのID
CKV = b"" # さっきのCKV

ID1 = ID[:8]
ID2 = ID[8:]

CKV1 = CKV[:8]
CKV2 = CKV[8:]

CK = b'' # カード鍵
CK1 = CK[:8]
CK2 = CK[8:]

RC = b'' # さっきの16バイトの乱数
RC1 = RC[:8]
RC2 = RC[8:]

SK1 = b'' # さっきのセッション鍵1
SK2 = b'' # セッション鍵2

SK1 = Crypto.Cipher.DES.new(int.from_bytes(SK1, byteorder="big").to_bytes(8, byteorder="little"), Crypto.Cipher.DES.MODE_ECB)
SK2 = Crypto.Cipher.DES.new(int.from_bytes(SK2, byteorder="big").to_bytes(8, byteorder="little"), Crypto.Cipher.DES.MODE_ECB)

MAC_A = SK1.encrypt(SK2.decrypt(SK1.encrypt((0xFFFF009100860082 ^ int.from_bytes(RC1, byteorder="little")).to_bytes(8, byteorder="big"))))

for i in [ID1, ID2, CKV1, CKV2]:
    temp = (int.from_bytes(i, byteorder="little") ^ int.from_bytes(MAC_A, byteorder="big")).to_bytes(8, byteorder="big")
    MAC_A = SK1.encrypt(SK2.decrypt(SK1.encrypt(temp)))

MAC_A = int.from_bytes(MAC_A, byteorder="big").to_bytes(8, byteorder="little") # もし正しいICカードならMAC_Aブロックに生成される値です

これでカードリーダー側でもMACが生成できたので

if MAC_A_Card == MAC_A:
    print("正しいICカードです!")
    # ロックの解除など...
else:
    print("間違ったICカードです...")

みたいな感じで使えます!

電子鍵的なやつのコード

繋げてみます

gen_cardkey.py
import os
import secrets
import Crypto.Cipher.DES

def gen_key(ID, master):
    M = ID
    key = master
    zero = "\x00" * 8
    KA = key[:8] # 8ビットずつに分けて
    KB = key[8:16]
    KC = key[16:]
    Ka = Crypto.Cipher.DES.new(KA, Crypto.Cipher.DES.MODE_ECB) # 鍵の定義
    Kb = Crypto.Cipher.DES.new(KB, Crypto.Cipher.DES.MODE_ECB)
    Kc = Crypto.Cipher.DES.new(KC, Crypto.Cipher.DES.MODE_ECB)

    def triple_DES(a, b, c, plain):
        return c.encrypt(b.decrypt(a.encrypt(plain))) # plainをaで暗号化してbで復号化してcで暗号化する

    L = triple_DES(Ka, Kb, Kc, zero)
    L = int.from_bytes(L, byteorder="big")

    if len(bin(L)) == 66: #最上位ビットが1
        K1 = L << 1
        K1 = K1 & 0xFFFFFFFFFFFFFFFF # 0b1111111111111111111111111111111111111111111111111111111111111111
        K1 = K1 ^ 0x1B
    else:
        K1 = L << 1
        K1 = K1 & 9223372036854775807 # 0b111111111111111111111111111111111111111111111111111111111111111

    M1  = M[:8]
    M2_ = int.from_bytes(M[8:], byteorder="big")
    M2 = K1 ^ M2_
    C1 = triple_DES(Ka, Kb, Kc, M1)
    T = triple_DES(Ka, Kb, Kc, (int.from_bytes(C1, byteorder="big") ^ M2).to_bytes(8,  byteorder="big"))
    M1_ = (int.from_bytes(M1, byteorder="big") ^ 0x8000000000000000).to_bytes(8,  byteorder="big")
    C1_ = triple_DES(Ka, Kb, Kc, M1_)
    T_ = triple_DES(Ka, Kb, Kc, (int.from_bytes(C1_, byteorder="big") ^ M2).to_bytes(8,  byteorder="big"))

    C = T + T_
    return C
gen_mac.py
import Crypto.Cipher.DES

def mac_a(RC, CK, ID, CKV):

    RC1 = RC[:8]
    RC2 = RC[8:]
    CK1 = CK[:8]
    CK2 = CK[8:]
    ID1 = ID[:8]
    ID2 = ID[8:]
    CKV1 = CKV[:8]
    CKV2 = CKV[8:]

    RC1_ = int.from_bytes(RC1, byteorder="big").to_bytes(8, byteorder="little")
    CK1 = Crypto.Cipher.DES.new(int.from_bytes(CK1, byteorder="big").to_bytes(8, byteorder="little"), Crypto.Cipher.DES.MODE_ECB)
    CK2 = Crypto.Cipher.DES.new(int.from_bytes(CK2, byteorder="big").to_bytes(8, byteorder="little"), Crypto.Cipher.DES.MODE_ECB)

    SK1_ = CK1.encrypt(CK2.decrypt(CK1.encrypt(RC1_)))
    SK1 = int.from_bytes(SK1_, byteorder="big").to_bytes(8, byteorder="little")
    RC2_ = int.from_bytes(RC2, byteorder="little")
    RC2_ = RC2_ ^ int.from_bytes(SK1_, byteorder="big")

    SK2_ = CK1.encrypt(CK2.decrypt(CK1.encrypt(RC2_.to_bytes(8, byteorder="big"))))
    SK2 = int.from_bytes(SK2_, byteorder="big").to_bytes(8, byteorder="little")

    SK1 = Crypto.Cipher.DES.new(int.from_bytes(SK1, byteorder="big").to_bytes(8, byteorder="little"), Crypto.Cipher.DES.MODE_ECB)
    SK2 = Crypto.Cipher.DES.new(int.from_bytes(SK2, byteorder="big").to_bytes(8, byteorder="little"), Crypto.Cipher.DES.MODE_ECB)

    MAC_A = SK1.encrypt(SK2.decrypt(SK1.encrypt((0xFFFF009100860082 ^ int.from_bytes(RC1, byteorder="little")).to_bytes(8, byteorder="big"))))

    for i in [ID1, ID2, CKV1, CKV2]:
        temp = (int.from_bytes(i, byteorder="little") ^ int.from_bytes(MAC_A, byteorder="big")).to_bytes(8, byteorder="big")
        MAC_A = SK1.encrypt(SK2.decrypt(SK1.encrypt(temp)))

    return int.from_bytes(MAC_A, byteorder="big").to_bytes(8, byteorder="little")
main.py
import binascii
import nfc
import os
import secrets

import gen_mac
import gen_cardkey

class MyCardReader(object):
    def __init__(self):
        self.master = b"" # 1次発行のときに使った個別化マスター鍵。ハードコーディングはあんまり良くないかも

    def on_connect(self, tag):
        self.sc_read = nfc.tag.tt3.ServiceCode(0, 0x0b)
        self.sc_write = nfc.tag.tt3.ServiceCode(0, 0x09)
        RC = secrets.token_bytes(16)
        bc80 = nfc.tag.tt3.BlockCode(0x80, service=0)
        tag.write_without_encryption([self.sc_write], [bc80], RC)

        bc82 = nfc.tag.tt3.BlockCode(0x82, service=0) # ID
        bc86 = nfc.tag.tt3.BlockCode(0x86, service=0) # CKV
        bc91 = nfc.tag.tt3.BlockCode(0x91, service=0) # MAC_A

        ret = tag.read_without_encryption([self.sc_read], [bc82, bc86, bc91])
        ID = bytes(ret[:16])
        CKV = ret[16:32]
        MAC_A_Card = ret[32:40]

        CK = gen_cardkey.gen_key(ID, self.master)

        MAC_A = gen_mac.mac_a(RC, CK, ID, CKV)

        if MAC_A_Card == MAC_A:
            print("あなたは正しい鍵を持っています!ロックを解除しています...")
            # 解除するプログラム
        else:
            print("あなたは不正な鍵を使用しようとしています")
            # 処理することがあれば

        return True

    def read_id(self):
        clf = nfc.ContactlessFrontend('usb')
        try:
            clf.connect(rdwr={'on-connect': self.on_connect})
        finally:
            clf.close()

if __name__ == '__main__':
    cr = MyCardReader()
    while True:
        cr.read_id() 

正しい鍵と正しいIDが入っていなければ認証が通らないのでIDmによる認証は必要ではないと思います。多分。(鍵はWrite Only)
また、Pollingコマンドはnfcpyが勝手に打ってくれるので不要です。

今作ったモジュールで1次発行のコードを書くと

first.py
import binascii
import nfc
import os
import secrets

import gen_mac
import gen_cardkey

class MyCardReader(object):
    def __init__(self):
        self.master = b""

    def on_connect(self, tag):
        sc_read = nfc.tag.tt3.ServiceCode(0, 0x0b)
        sc_write = nfc.tag.tt3.ServiceCode(0, 0x09)

        ID_six = os.urandom(6) # かぶる可能性あるかも
        ID = b"\x00"*10 + ID_six # write_without_encryptionに使います。
        bc82 = nfc.tag.tt3.BlockCode(0x82, service=0)
        tag.write_without_encryption([sc_write], [bc82], ID)

        print(bytes(tag.idm) + b"\x00\x00" + ID_six)

        CK = gen_cardkey.gen_key(bytes(tag.idm) + b"\x00\x00" + ID_six, self.master) # IDmとIDdは同じです。
        bc87 = nfc.tag.tt3.BlockCode(0x87, service=0)
        tag.write_without_encryption([sc_write], [bc87], CK)

        CKV = b"\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
        bc86 = nfc.tag.tt3.BlockCode(0x86, service=0)
        tag.write_without_encryption([sc_write], [bc86], CKV)

        print("書き込みました")

        return True

    def read_id(self):
        clf = nfc.ContactlessFrontend('usb')
        try:
            clf.connect(rdwr={'on-connect': self.on_connect})
        finally:
            clf.close()

if __name__ == '__main__':
    cr = MyCardReader()
    while True:
        cr.read_id()

こんな感じになるかな?と思います

19
16
4

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
19
16