MACつき読み出し・書き込みを実装していきます
読み出しについては内部認証、書き込みについては外部認証で実装したコードに酷似しています。
本記事では、Felica Lite-Sを主として扱っています。
前提
ユーザーズマニュアル3章 ブロックの21ページにあるブロックについての情報
また、内部認証・外部認証を実装する過程で作成済みのスクリプトは前提とさせていただきます。
- 独自の型
- 個別化暗号鍵の生成・内部認証・外部認証を行うクラス
の2点を実装しています。
bytes
型とint
型を相互に操作するため、int
がbytes
に変換されていることを前提として型を作成しました。bytes
を基底とし、<<
,>>
,&
,^
,スライス
,型変換
を定義しています。
個別化暗号鍵の生成・内部認証・相互認証を実装したクラスです
import secrets
import nfc
from nfc.tag.tt3 import BlockCode, ServiceCode, Type3Tag
from system import intbytes
from key import MASTER_KEY
import Crypto.Cipher.DES as crypto_des
from pyDes import triple_des,CBC
import secrets
from nfc.tag.tt3 import Type3Tag,BlockCode,ServiceCode
sc_read = ServiceCode(0, 0x0b) # データを読み込むときに使います
sc_write = ServiceCode(0, 0x09) # データを書き込むときに使います
bc80 = BlockCode(0x80, service=0)
bc82 = BlockCode(0x82, service=0)
bc86 = BlockCode(0x86, service=0)
bc90 = BlockCode(0x90, service=0)
bc91 = BlockCode(0x91, service=0)
bc92 = BlockCode(0x92, service=0)
class MyFelicaLiteS(Type3Tag):
def __init__(self, clf, target):
"""
```
clf = nfc.ContactlessFrontend("usb")
tag:FelicaLiteS = clf.connect(rdwr={'on-connect': lambda tag: False})
tag:MyFelicaLiteS = MyFelicaLiteS(clf,tag.target)
```
"""
super().__init__(clf, target)
self.master_key = None
def add_masterKey(self,master_key):
self.master_key = master_key
def authenticate(self):
assert self.master_key is not None, "Master key must be set using add_masterKey before using this method."
# 乱数RC = RC1[1]-[7] + RC2[1]-[7]
self.RC = secrets.token_bytes(16)
# ランダムチャレンジブロックにRCを書き込む
self.write_without_encryption([sc_write], [bc80], self.RC)
# ID,CKV,WCNT,MAC_Aを同時に読み出す
# MAC_Aの値は最後に読み出す
block_list = [bc82, bc86, bc90, bc91]
ret = self.read_without_encryption([sc_read], block_list)
ID = ret[:16]
WCNT = ret[32:36] # Felica Lint Lite-Sモードに対応し、4バイトとする
MAC_A_card = ret[48:56]
# カード鍵をIDから生成
CK = self.generate_CK(id= ID)
# Rとカード鍵からセッション鍵SKを作成
_sk = triple_des(CK[7::-1]+CK[:7:-1],CBC,b'\x00'*8).encrypt(self.RC[7::-1]+self.RC[:7:-1])
self.SK1, self.SK2 = _sk[7::-1],_sk[:7:-1]
# MAC_Aの生成
plain = b'\x82\x00\x86\x00\x90\x00\x91\x00'
MAC_A = self._generate_mac(plain, self.SK1, self.SK2, ret[:-16])
if not MAC_A_card == MAC_A:
return False
# 外部認証
# stateに書き込むデータの作成
state_data = b"\x01".ljust(16,b"\x00")
# WCNTとブロック番号からplainの生成
plain = WCNT.ljust(4,b"\x00") + bc92.number.to_bytes(1,"big") + b"\x00\x91\x00"
# plain,R,SK,ブロックデータからMACの生成
MAC_A_w = self._generate_mac(plain, self.SK2, self.SK1, state_data)
# MAC_Aブロックに書き込む値の作成
w_block_data = state_data + (MAC_A_w + WCNT).ljust(16, b'\x00')
self.write_without_encryption([sc_write], [bc92,bc91],w_block_data)
return True
def generate_CK(self,id):
zero = b"\x00" * 8
M = id
K = self.master_key
L = int.from_bytes(triple_des(K,CBC,zero).encrypt(zero),"big")
# ドキュメントより:左シフトとは、値を2倍しその最上位bitを捨てること
K1 = L << 1
K1 = K1 & 0xFFFFFFFFFFFFFFFF # 左方シフトにより64bitを超えた範囲は取り除く
if L >> 63 == 1:
K1 = K1 ^ 0x1B
M1 = M[:8]
M2_ = M[8:]
M2 = (int.from_bytes(M2_, "big") ^ K1).to_bytes(8,"big")
# C1 -> T,C1' -> T'の生成は同時に行なっています
M = M1 + M2
T = triple_des(K,CBC,zero).encrypt(M)[8:] # 6),7)
T_ = triple_des(K,CBC,b"\x80\00\00\00\00\00\00\00").encrypt(M)[8:]
C = T + T_
return C
def _generate_mac(self,plain,key1,key2,block_data):
"""_summary_
Args:
plain (bytes): 平文
key1 (bytes): ENC(暗号化)キー
key2 (bytes): DEC(複合化)キー
block_data (bytes): 生成に必要なデータのみとすること
Returns:
bytes: MACの値
"""
data = b"".join([block_data[i:i+8][::-1] for i in range(0,len(block_data),8)])
return triple_des(key1[::-1]+key2[::-1],CBC,self.RC[7::-1]).encrypt(plain[::-1] + data)[:-9:-1]
MACなし読み出し・書き込み
いきなり本題と外れてしまいますが、nfcpy本来の仕様だと少し扱いづらい側面があるのでこの機会に作成します。
def read_without_mac(self, *blocks:int):
"""read_without_encryptionの簡易版
Args:
blocks (int):0x92のような形式で指定,最大サイズ4
Returns:
bytearray:
"""
assert len(blocks) <= 4
service_list = [ServiceCode(0, 0x0b)]
block_list = [BlockCode(n) for n in blocks]
return self.read_without_encryption(service_list, block_list)
def write_without_mac(self, block_list:list[int], data:bytes):
"""write_without_encryptionの簡易版
Args:
block_list (list[int]): 最大サイズ2
data (bytes): データ
"""
assert len(data) == 16 * len(block_list) and len(block_list) <=2
sc_list = [ServiceCode(0, 0x09)]
bc_list = [BlockCode(n) for n in block_list]
self.write_without_encryption(sc_list, bc_list, data)
MACつき読み出し
処理の流れ
内部認証の実装ではRC1
の生成とセッション鍵SK
の生成の過程がありました。
MACつき読み出してでは内部認証が完了していることが前提となっており、これらの値は生成済みとなっている点が内部認証の実装と異なっています。
よって内部認証の処理を一部書き換えることで実装可能です。
実装
MACつき読み取りの処理
内部認証の処理を一部取り出して調節しただけですので、細かくは書きません。詳しくは内部認証の実装を読んでいただけたらと思います。
def read_with_mac(self,*blocks:tuple[int]) -> bytearray:
"""MACつき読み出しを行う。
Args:
blocks(int) : 0x92のような形式で指定,最大サイズ3
Raises:
Exception: MAC値が一致しない(改竄検知)
Returns:
bytearray: 読み出したデータ
"""
assert len(blocks) <= 3
assert self.RC is not None , "内部認証を行なってください"
# データと,MAC_Aを同時に読み出す
block_list = list(blocks) + [0x91]
ret = self.read_without_mac(*block_list)
data = ret[:-16]
MAC_A_card = ret[-16:-8]
# MAC_Aの生成
plain = b"".join([n.to_bytes(1,"big") + b"\00" for n in block_list]).ljust(8,b'\xFF')
MAC_A = self._generate_mac(plain, self.SK1, self.SK2, ret[:-16])
if MAC_A_card == MAC_A:
return data
else:
raise Exception("MAC値が一致しません")
MACつき書き込み
処理の流れ
こちらも同様に内部認証が終了していることが前提となっています。
R
,SK
はMACつき読み込み実装時、インスタンス変数に追加済みです。
実装
ここで外部認証の実装に立ち返ってみます。外部認証の要件は以下の通りでした
- 内部認証時に
WCNT
の値を読み出せ -
WCNT
の値を利用し、STATEブロックに対するMACつき書き込みを実装せよ
つまり、MACつき書き込みの処理は作成済みということです。この部分だけを抜き出します。
WCNT
の値は本実装では引数としています。インスタンス変数で管理することも考えましたが、write_without_encrypitonコマンドをインスタンスを通さず呼び出すことでも値が変化しますので、これを管理する実装は主題から外れると判断しました。
def write_with_mac(self, block_code:int, data:bytes, wcnt:bytes = None):
"""MACつき書き込みを行う
Args:
block_code (int): 対象ブロックのブロックコード
data (bytes): _description_
wcnt (bytes, optional): 書き込み時のwcntの値.指定されなければ取得する.
"""
assert len(data) == 16 and type(block_code) == int
if not wcnt:
wcnt = self.read_without_mac(0x90)[0:4]
sc_list = [ServiceCode(0, 0x09)]
bc_list = [BlockCode(block_code),BlockCode(0x91)]
plain = wcnt.ljust(4, b"\x00") + block_code.to_bytes(1,"big") + b"\x00\x91\x00"
MAC = self._generate_mac(plain, self.SK2, self.SK1, data)
w_block_data = data + (MAC + wcnt).ljust(16, b'\x00')
self.write_without_encryption(sc_list,bc_list,w_block_data)
作成済み処理の書き直し
本題からは外れますが、前回までの内容の色々な部分を書き直すことができます。
from nfc.tag.tt3 import BlockCode, ServiceCode, Type3Tag
from nfc.tag.tt3_sony import FelicaLiteS
import nfc
import secrets
from pyDes import triple_des, CBC
from key import MASTER_KEY
class MyFelicaLiteS(Type3Tag):
def __init__(self, clf, target):
"""
```
clf = nfc.ContactlessFrontend("usb")
tag:FelicaLiteS = clf.connect(rdwr={'on-connect': lambda tag: False})
tag:MyFelicaLiteS = MyFelicaLiteS(clf,tag.target)
```
"""
super().__init__(clf, target)
self.master_key = None
def add_masterKey(self,master_key):
self.master_key = master_key
def authenticate(self):
assert self.master_key is not None, "Master key must be set using add_masterKey before using this method."
# 内部認証
# ランダムチャレンジブロックに書き込み
self.RC = secrets.token_bytes(16)
self.write_without_mac([0x80],self.RC)
# ID,CKV,WCNT,MAC_Aを同時に読み出す
block_list = [0x82,0x86,0x90,0x91]
ret = self.read_without_mac(*block_list)
ID = ret[:16]
WCNT = ret[32:36] # Felica Lint Lite-Sモードに対応し、4バイトとする
MAC_A_card = ret[48:56]
# カード鍵をIDから生成
CK = self.generate_CK(id= ID)
# Rとカード鍵からセッション鍵SKを作成
_sk = triple_des(CK[7::-1]+CK[:7:-1],CBC,b'\x00'*8).encrypt(self.RC[7::-1]+self.RC[:7:-1])
self.SK1, self.SK2 = _sk[7::-1],_sk[:7:-1]
# MAC_Aの生成
plain = b'\x82\x00\x86\x00\x90\x00\x91\x00'
MAC_A = self._generate_mac(plain, self.SK1, self.SK2, ret[:-16])
# 内部認証に失敗したらその時点で終了
if not MAC_A_card == MAC_A:
return False
# 外部認証(内部認証と合わせて相互認証となる)
# stateに書き込むデータの作成
state_data = b"\x01".ljust(16,b'\x00')
# stateの値をMACつき書き込み
self.write_with_mac(0x92,state_data,WCNT)
return True
def generate_CK(self,id):
assert self.master_key is not None, "Master key must be set using add_masterKey before using this method."
zero = b"\x00" * 8
K = self.master_key
L = int.from_bytes(triple_des(K,CBC,zero).encrypt(zero),"big")
K1 = (L<<1)& 0xFFFFFFFFFFFFFFFF
if L >> 63 == 1:
K1 = K1 ^ 0x1B
M = id[:8] + (int.from_bytes(id[8:],"big") ^ K1).to_bytes(8,"big")
T = triple_des(K,CBC,zero).encrypt(M)[8:]
T_ = triple_des(K,CBC,b"\x80\00\00\00\00\00\00\00").encrypt(M)[8:]
return T + T_
def read_without_mac(self, *blocks:int):
"""read_without_encryptionの簡易版
Args:
blocks (int):0x92のような形式で指定,最大サイズ4
Returns:
bytearray:
"""
assert len(blocks) <= 4
service_list = [ServiceCode(0, 0x0b)]
block_list = [BlockCode(n) for n in blocks]
return self.read_without_encryption(service_list, block_list)
def write_without_mac(self, block_list:list[int], data:bytes):
"""write_without_encryptionの簡易版
Args:
block_list (list[int]): 最大サイズ2
data (bytes): データ
"""
assert len(data) == 16 * len(block_list) and len(block_list) <=2
sc_list = [ServiceCode(0, 0x09)]
bc_list = [BlockCode(n) for n in block_list]
self.write_without_encryption(sc_list, bc_list, data)
def write_with_mac(self, block_code:int, data:bytes, wcnt:bytes = None):
"""MACつき書き込みを行う
Args:
block_code (int): 対象ブロックのブロックコード
data (bytes): _description_
wcnt (bytes, optional): 書き込み時のwcntの値.指定されなければ取得する.
"""
assert len(data) == 16 and type(block_code) == int
if not wcnt:
wcnt = self.read_without_mac(0x90)[0:4]
sc_list = [ServiceCode(0, 0x09)]
bc_list = [BlockCode(block_code),BlockCode(0x91)]
plain = wcnt.ljust(4, b"\x00") + block_code.to_bytes(1,"big") + b"\x00\x91\x00"
MAC = self._generate_mac(plain, self.SK2, self.SK1, data)
w_block_data = data + (MAC + wcnt).ljust(16, b'\x00')
self.write_without_encryption(sc_list,bc_list,w_block_data)
def read_with_mac(self,*blocks:tuple[int]) -> bytearray:
"""MACつき読み出しを行う
Args:
blocks(int) : 0x92のような形式で指定,最大サイズ3
Raises:
Exception: MAC値が一致しない(改竄検知)
Returns:
bytearray: 読み出したデータ
"""
assert len(blocks) <= 3
assert self.RC is not None , "内部認証を行なってください"
# データと,MAC_Aを同時に読み出す
block_list = list(blocks) + [0x91]
ret = self.read_without_mac(*block_list)
data = ret[:-16]
MAC_A_card = ret[-16:-8]
# MAC_Aの生成
plain = b"".join([n.to_bytes(1,"big") + b"\00" for n in block_list]).ljust(8,b'\xFF')
MAC_A = self._generate_mac(plain, self.SK1, self.SK2, ret[:-16])
if MAC_A_card == MAC_A:
return data
else:
raise Exception("MAC値が一致しません")
def _generate_mac(self,plain,key1,key2,block_data):
"""_summary_
Args:
plain (bytes): 平文
key1 (bytes): ENC(暗号化)キー
key2 (bytes): DEC(複合化)キー
block_data (bytes): 生成に必要なデータのみとすること
Returns:
bytes: MACの値
"""
data = b"".join([block_data[i:i+8][::-1] for i in range(0,len(block_data),8)])
return triple_des(key1[::-1]+key2[::-1],CBC,self.RC[7::-1]).encrypt(plain[::-1] + data)[:-9:-1]
最後に
Felica Lite-Sに対する内部認証・外部認証・MACつき読み出し・MACつき書き込みの実装が完了しました。
今回はWCNT
の値を引数とし、管理することは放棄しました。
「僕の考えた最強のFelica Lite-Sクラス」を実装することがあれば、この辺も綺麗にしたいです。
参考にしたサイト
今回参考にしたサイト(公式ドキュメント)
内部認証の実装で参考にした記事