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 = "\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次発行はやらなくても構いません。(内部認証に関係ない)
内部認証をする
流れの説明
こんな感じで認証をします。
ここで使われているMAC_Aは
こんな感じで生成します(理解に時間がかかった...)
☆印のところでバイトオーダーを反転させます。
また、⊕は排他的論理和です。
れっつコーディング
全部一つにまとめるとわかりにくくなるのでいろいろ分割します
まず、チャレンジブロック(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
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カードです...")
みたいな感じで使えます!
電子鍵的なやつのコード
繋げてみます
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
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")
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次発行のコードを書くと
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()
こんな感じになるかな?と思います