【python】NFC Felica Lite-S 相互認証(外部認証)の実装

[内部認証の実装](https://qiita.com/Nekoq0o0p/items/4be549fef0127ce55fca)の記事に続けて、相互認証を実装していきます。相互認証は内部認証につづき外部認証を行うことですので、本記事では外部認証の実装を主に記載しています。（内部認証は以前の記事をご覧ください）

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

# 前提
前回の[内部認証の実装](https://qiita.com/Nekoq0o0p/items/4be549fef0127ce55fca)をベースとしています。
作成済みコードのみ記載しますが、詳細は記事をご覧ください。

<details>

```felica_lite_s.py
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 internal_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,MAC_Aを同時に読み出す
        # MAC_Aの値は最後に読み出す
        block_list = [bc82, bc86, bc91]
        ret = self.read_without_encryption([sc_read], block_list)
        ID = ret[:16]
        MAC_A_card = ret[32:40]

        # カード鍵を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\x91\x00\xFF\xFF'
        MAC_A = self._generate_mac(plain, self.SK1, self.SK2, ret[:-16])

        return  MAC_A_card == MAC_A
    
    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]


```


```internal_authenticate.py
from test_class import MyFelicaLiteS
import nfc
from nfc.tag.tt3 import Type3Tag
from key import MASTER_KEY

clf = nfc.ContactlessFrontend("usb")
tag:Type3Tag = clf.connect(rdwr={'on-connect': lambda tag: False})

tag = MyFelicaLiteS(clf,tag.target)

tag.add_masterKey(master_key=MASTER_KEY)
print(tag.internal_authenticate())
```


</details>

# 相互認証(外部認証)の実装
[マニュアル](https://www.sony.co.jp/Products/felica/business/tech-support/data/fls_usmnl_1.4j.pdf)を頼りに実装していきます
## 処理の流れ
下記画像を簡単に要約すると、
- 内部認証時に`WCNT`の値を読み出せ
- `WCNT`の値を利用し、STATEブロックに対するMACつき書き込みを実装せよ

とのことです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629819/fb50ba36-cc52-684b-69c3-77e813f120e6.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629819/059f361a-22f0-6f0f-96fc-663f2d45b407.png)


## 内部認証の実装

相互認証を実施するにあたって、内部認証で値を読み出す際に`WCNT`の値を読み出しておく必要があります。

```python
# 内部認証
# WCNTを読み出す以外はinternal_authenticationと同じ
R = self._write_random_challenge_block()

# ID,CKV,WCNT,MAC_Aを同時に読み出す
# ここでWCNTを読み出しているのが唯一の変更点です
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])
```

## 外部認証の実装
### WCNTとブロック番号とブロックデータからMACの生成
ここでのブロックとはSTATEブロックのことです。
```python
# 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の構造はこのようになっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629819/950af756-cbe1-5e8b-cdad-acc409da3ef8.png)

:::note 
`リトルエンディアン`は実装に関係がないです。
:::
また、マニュアル4.4.4 Write Without Encrypitonにこのように記載があります。
> ブロック数が2の場合、2ブロック目のブロック番号が91h(MAC_A)に設定されていること

以上を踏まえてコードを作成します。

```python
# 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)
```

なお、外部認証の書き込みに失敗した場合には以下のエラーが発生します。

```terminal
nfc.tag.tt3.Type3TagCommandError: verification failure for write with mac operation
```

### 状態の確認
認証に成功すると、STATEの値が変化します。
この項の内容は特に`Felica Lite-S`に特異的なものと思われます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629819/9670566e-2397-52d3-18b0-6be0f823d015.png)

これを参考にSTATEの値を読み取り状態を出力します。（この処理自体は認証処理には関係ありません。）

```python
# state の値を確認
state = self.read_without_encryption([self.sc_read],[self.bc92])
state = state[0:2]
state_msg = f"外部(相互)認証: {'未' if state[0] == 0 else ''}完了\nPollingコマンド: {'非' if state[1] == 1 else ''}応答"
print(state_msg)
```

# 作成したコード
長くなるので、前提で作成したクラスに記載してある部分についてはカットします
```felica_lite_s.py
class MyFelicaLiteS(Type3Tag):

    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
    
    
```

# 最後に
今回何気なくMACつき書き込みの処理が紛れ込んでいました。
次はMACつきの書き込み、読み出しを実装したいです。

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

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

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

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