【python】NFC Felica Lite-S MACつき読み出し・書き込みの実装

MACつき読み出し・書き込みを実装していきます
読み出しについては[内部認証](https://qiita.com/Nekoq0o0p/items/4be549fef0127ce55fca)、書き込みについては[外部認証](https://qiita.com/drafts/3bf1649a4c158f2ff66a)で実装したコードに酷似しています。

:::note warn
本記事では、Felica Lite-Sを主として扱っています。
:::


# 前提
[ユーザーズマニュアル3章 ブロック](https://www.sony.co.jp/Products/felica/business/tech-support/data/fls_usmnl_1.4j.pdf)の21ページにあるブロックについての情報
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629819/2dfe59f3-842d-9028-9b6a-ce611a487f55.png)

また、内部認証・外部認証を実装する過程で作成済みのスクリプトは前提とさせていただきます。
- 独自の型
- 個別化暗号鍵の生成・内部認証・外部認証を行うクラス

の2点を実装しています。
<details>

`bytes`型と`int`型を相互に操作するため、`int`が`bytes`に変換されていることを前提として型を作成しました。`bytes`を基底とし、`<<`,`>>`,`&`,`^`,`スライス`,`型変換`を定義しています。

個別化暗号鍵の生成・内部認証・相互認証を実装したクラスです

```felica_lite_s.py

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]

```

</details>

# MACなし読み出し・書き込み
いきなり本題と外れてしまいますが、nfcpy本来の仕様だと少し扱いづらい側面があるのでこの機会に作成します。

```python
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つき読み出してでは内部認証が完了していることが前提となっており、これらの値は生成済みとなっている点が内部認証の実装と異なっています。
よって内部認証の処理を一部書き換えることで実装可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629819/3acaab37-ac2f-6b03-37bb-512ebebb43e4.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314089/0fd7abba-c9b2-856e-ce14-eef6cd9550a2.png)


## 実装
### MACつき読み取りの処理
内部認証の処理を一部取り出して調節しただけですので、細かくは書きません。詳しくは内部認証の実装を読んでいただけたらと思います。

```python
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つき読み込み実装時、インスタンス変数に追加済みです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629819/68196266-0b34-b4ed-ed69-b0fda41f6b69.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629819/059f361a-22f0-6f0f-96fc-663f2d45b407.png)


## 実装

ここで外部認証の実装に立ち返ってみます。外部認証の要件は以下の通りでした
- 内部認証時に`WCNT`の値を読み出せ
- `WCNT`の値を利用し、STATEブロックに対するMACつき書き込みを実装せよ

つまり、MACつき書き込みの処理は作成済みということです。この部分だけを抜き出します。
`WCNT`の値は本実装では引数としています。インスタンス変数で管理することも考えましたが、write_without_encrypitonコマンドをインスタンスを通さず呼び出すことでも値が変化しますので、これを管理する実装は主題から外れると判断しました。

```python
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)
```
## 作成済み処理の書き直し
本題からは外れますが、前回までの内容の色々な部分を書き直すことができます。
```python
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クラス**」を実装することがあれば、この辺も綺麗にしたいです。

# 参考にしたサイト
今回参考にしたサイト（公式ドキュメント）

https://www.sony.co.jp/Products/felica/business/tech-support/

内部認証の実装で参考にした記事

https://qiita.com/jun50/items/01debfe8655d6f0dfd4a