【python】NFC Felica Lite-S 一次発行(仮)~内部認証の実装

felica認証の実装を解説している記事があまりなかったので記録として残しました。

:::note warn
本記事では、Felica Lite-Sを主として扱っています。
:::
公式ドキュメントをベースに進めていきます。
本記事は大まかに以下の構成となっています。
1. 一次発行~felicaカードにIDを書き込む~
1. 内部認証スクリプトの作成
1. 作成したスクリプトのまとめ


# 前提とするもの

### ブロック情報
[ユーザーズマニュアル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)


### 暗号化方法(Triple DES)
Felica Lite-SのMAC生成ではTriple DESの中でも[Cipher block chaining(CBC)](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_block_chaining_(CBC))を採用しています。CBCとは、以下のようなTriple DESの形態です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629819/7cc5274f-214b-095f-7002-84a8b0fcd218.png)

これを実装するため本スクリプトでは`pyDes`ライブラリの`triple_des`,`CBC`モードを利用します。以下は動くコードではありませんが、利用方法のイメージに役立つとはずです。

```python
from pyDes import triple_des, CBC
plains = b"plain1" + b"plain2"+ ...
key = b"key1" + b"key2" + b"key3"
key = b"key1" + b"key2" # この場合key3としてkey1が利用されます
Ciphertexts = triple_des(key= key, mode= CBC, IV= iv).encrypt(plains)
# Ciphertext1 = Ciphertexts[0:8]
# Ciphertexts = "Ciphertext1" + "Ciphertext2" + ...
```
### 前提のスクリプト
以下に記載するスクリプトは、ファイル上に書き込まれているものとして扱います。

ServiceCode,BlockCodeの指定と、tagの読み込みです。
ServiceCodeについては、マニュアル3.2に詳しく記載があります。
BlockCode(`bc`)は、記事内で出現する全ての記載をしていません。
```python
import nfc
from nfc.tag.tt3 import BlockCode, ServiceCode, Type3Tag

sc_read = ServiceCode(0, 0x0b) # データを読み込むときに使います
sc_write = ServiceCode(0, 0x09) # データを書き込むときに使います
bc00 = BlockCode(0x00, service=0)
clf = nfc.ContactlessFrontend("usb")
tag:Type3Tag = clf.connect(rdwr={'on-connect': lambda tag: False})
```



# 一次発行
内部認証の実装に必要な最低限の実装のみ行います。
[ユーザーズマニュアル7章 発行](https://www.sony.co.jp/Products/felica/business/tech-support/data/fls_usmnl_1.4j.pdf)によると、主に以下の手順で行うようです。
1. IDの書き込み
1. カード鍵CKの書き込み
1. カード鍵バージョンCKVの書き込み

実運用ではこれに加えて「システムブロックの書き換え禁止、各ブロックのアクセス権限、 NDEF 対応オプションの設定」があります。

## IDの書き込み
マニュアル3.1.6 ID によると、IDブロックは次のように構成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629819/0707553f-dd3a-272c-201c-f4bafa58b612.png)

出荷時は前半8バイトに`IDd`の値、後半8バイトに`ALL_00h`が格納されます。
`IDd`:
値の書き込みが可能となっていますが、不揮発性メモリー上に値は保存されません。したがって、値を書き込んでも、一度電源断すると初期値に戻ります。

`DFC`・`任意値`:
ID[8][9]にはDFCを格納してください。DFCを使用しない場合は、0000hを格納してください。残りの6バイトには任意の値を格納してください。

これらより、`ID`の生成、書き込みは以下のスクリプトになります
```python
import os

# IDの生成
ID = tag.idm + b"\x00" * 2 + os.urandom(6)  

# IDの書き込み
tag.write_without_encryption([sc_write], [bc82], ID)
```

## カード鍵の書き込み

`ID` + `個別化マスター鍵` から `カード鍵CK`を生成、書き込みます。
マニュアル3.1.11によると、`CK`の読み出し値は常に`ALL_00h`です。
これにより`個別化マスター鍵`がないと`CK`の値を求められず、認証できなくなっています。

[`CK`の生成アルゴリズム](https://www.sony.co.jp/Products/felica/business/tech-support/data/fl_card_key_algorithm_1.12.pdf)を見ながら、`CK`を生成するスクリプトを作成します。

```python
from pyDes import triple_des, CBC
import secrets

# 一度生成したら大切に保存し利用してください
# MASTER_KEY = secrets.token_bytes(24)
MASTER_KEY = b""

def generate_CK(id):
    zero = b"\x00" * 8
    
    M   = id
    K   = 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
```

これを用いて`CK`を書き込みます

```python
# card_keyの生成
CK = generate_CK(ID)

# card_keyの書き込み
tag.write_without_encryption([sc_write], [bc87], CK)
```

## カード鍵バージョンの書き込み
先頭2byteのみが任意の値に書き換え可能です。
```python
CKV = b"\x00\x01"+b"\x00"*14 # 上位2バイトの「\x00\x01」部分のみ任意の値にできる
tag.write_without_encryption([sc_write], [bc86], CKV)
```



# 内部認証の作成
## 内部認証の流れ
詳細はマニュアル5章に記載があります。

5.4.1より
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314089/68be010d-753c-294f-38c9-c92a4a714d43.png)

MAC_Aは以下の過程で生成されます。
セッション鍵の作成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314089/85885f1d-d4d5-ee7b-bc75-dd8d1804ca97.png)
MAC_Aの生成
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/314089/0fd7abba-c9b2-856e-ce14-eef6cd9550a2.png)

☆印のところでバイトオーダーを反転させます。
また、⊕は排他的論理和です。

## 実装
### ランダムチャレンジブロックへの書き込み
```python
import secrets
# 乱数RC = RC1[1]-[7] + RC2[1]-[7]
RC = secrets.token_bytes(16)
# ランダムチャレンジブロックにRCを書き込む
tag.write_without_encryption([sc_write], [bc80], RC)
```
### ID,CKV,MAC_Aの読み出す
MAC_Aの値は同時に読み出すもののうち最後に読み出す必要があります。
(マニュアル5.3に記載があります。おそらく値の計算が影響しています。)
```python
# MAC_Aの値は最後に読み出す
block_list = [bc82, bc86, bc91]
ret = tag.read_without_encryption([sc_read], block_list)
ID = ret[:16]
CKV = ret[16:32] 
MAC_A_card = ret[32:40]
```
### セッション鍵の生成
```python
# カード鍵をIDから生成
CK = generate_CK(id= ID)

# RCとカード鍵からセッション鍵SKを作成
_sk = triple_des(CK[7::-1] + CK[:7:-1], CBC, b'\x00' * 8).encrypt(RC[7::-1] + RC[:7:-1])
SK1, SK2 = _sk[7::-1], _sk[:7:-1]
```
### MAC_Aの生成とカード判定

ドキュメント内「読み出すブロックのブロック番号」の「読み出すブロック」とは、`ID`,`CKV`,`MAC`などを読み出したリクエストで読み出したブロックです（すなわち、今回であれば`ID`,`CKV`,`MAC`です）

```python
def _generate_mac(plain,key1,key2,block_data):
    """MACを生成する

    Args:
        plain (bytes): 1つ目の平文
        key1 (bytes): ENC(暗号化)キー
        key2 (bytes): DEC(複合化)キー
        block_data (bytes): 生成に必要なデータのみとすること（2つ目以降の平文）

    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,RC[7::-1]).encrypt(plain[::-1] + data)[:-9:-1]

# ID,CKV,SKからMAC_Aを生成
# 読み出したブロックのデータ=ID,CKV(MACを求める処理なのでMACは除く)である

# 図5-3の1番最初の平文(plain)
plain = b'\x82\x00\x86\x00\x91\x00\xFF\xFF'

MAC_A = _generate_mac(plain, SK1, SK2, ret[:-16])

if MAC_A_card == MAC_A:
    print("正しいICカードです！")
else:
    print("間違ったICカードです...")
```

# 作成したコード
実運用ではnfcリーダーの部分をもう少し丁寧かくと思いますが、主題ではないのでそのままです。


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


```

```first_writing.py
from felica_lite_s import MyFelicaLiteS
import nfc
from nfc.tag.tt3 import Type3Tag,ServiceCode,BlockCode
from key import MASTER_KEY
import os

sc_read = ServiceCode(0, 0x0b) # データを読み込むときに使います
sc_write = ServiceCode(0, 0x09) # データを書き込むときに使います
bc82 = BlockCode(0x82, service=0)
bc86 = BlockCode(0x86, service=0)
bc87 = BlockCode(0x87, service=0)

def first_writing(tag:MyFelicaLiteS,ID:bytes)->bytes:
    if len(ID) != 6:
        raise ValueError("ID must be 6 bytes")

    # IDの生成
    ID = tag.idm + b"\x00" * 2 +ID
    tag.add_masterKey(MASTER_KEY)

    # card_keyの生成
    CK = tag.generate_CK(ID)

    # IDの書き込み
    tag.write_without_encryption([sc_write], [bc82], ID)

    # card_keyの書き込み
    tag.write_without_encryption([sc_write], [bc87], CK)
    
    # card_key_versionの書き込み
    # 上位2バイトの「\x00\x01」部分のみ任意の値にできる
    # MASTER_KEYの値を変えるごとに更新するとされている
    CKV = b"\x00\x01"+b"\x00"*14 
    tag.write_without_encryption([sc_write], [bc86], CKV)

    return ID

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

tag = MyFelicaLiteS(clf,tag.target)

ID = first_writing(tag,os.urandom(6))
print(f"ID:{int.from_bytes(ID, 'big')}")
```

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


# 最後に
相互認証やMACつきの読み込み、書き込みなども記事にしていきたいです。

2024/10/14
相互認証作成しました

https://qiita.com/Nekoq0o0p/items/3bf1649a4c158f2ff66a
# 参考にしたサイト

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

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