Skip to content

Commit d4a38e2

Browse files
committed
Add pure SDM MAC helper
1 parent 3e859e4 commit d4a38e2

3 files changed

Lines changed: 107 additions & 22 deletions

File tree

src/schnee/adapters/ntag/core.py

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313
from schnee.adapters.backend.core import Backend
1414
from schnee.adapters.backend.pcsc import PcscApduClient, PcscBackend
1515
from schnee.adapters.ntag.apdu import Ntag424ApduPreset
16-
from schnee.adapters.ntag.crypt import aes_decrypt, aes_encrypt, xor_bytes
16+
from schnee.adapters.ntag.crypt import (
17+
aes_decrypt,
18+
aes_encrypt,
19+
calculate_sdm_mac,
20+
xor_bytes,
21+
)
1722
from schnee.adapters.ntag.utils import (
1823
Offset,
1924
build_ndef_url_file_data,
@@ -777,27 +782,12 @@ def verify_sdm_mac(
777782
# 通常のSDM実装では「Little Endian」として扱う必要があります。
778783
ctr_bytes_le = ctr_bytes[::-1]
779784

780-
# 2. セッション鍵生成用のSystem Vector (SV) の作成
781-
# SV = 3C C3 00 01 00 80 + UID(7bytes) + Counter(3bytes, Little Endian)
782-
# "3C C3 00 01 00 80" は NXP AN12196 で定義される定数(File 2の場合)
783-
sv_prefix = binascii.unhexlify("3CC300010080")
784-
sv_bytes = sv_prefix + uid_bytes + ctr_bytes_le
785-
786-
c_ses = CMAC.new(key_bytes, ciphermod=AES)
787-
c_ses.update(sv_bytes)
788-
session_key = c_ses.digest()
789-
790-
# 4. SDM-MAC の計算
791-
# 入力データは「空 (empty)」またはミラーされたデータそのもの。
792-
# 標準的なSDM設定(File Dataの暗号化なし)では、MACは空入力に対して計算されます。
793-
c_mac = CMAC.new(session_key, ciphermod=AES)
794-
c_mac.update(b"") # 空データ
795-
full_mac = c_mac.digest()
796-
797-
# 5. トランケーション (短縮)
798-
# フルCMAC(16バイト)から、奇数インデックス(1, 3, 5...)のバイトを抽出して8バイトにします。 # noqa: E501
799-
# (仕様書では "Even bytes" と書かれることがありますが、実装上は [1::2] が正解となるケースが大半です) # noqa: E501
800-
calculated_mac = full_mac[1::2]
785+
calculated_mac = calculate_sdm_mac(
786+
sdm_key=key_bytes,
787+
signed_data=b"",
788+
uid=uid_bytes,
789+
counter=ctr_bytes_le,
790+
)
801791

802792
print(f"Calculated MAC: {calculated_mac.hex().upper()}")
803793
print(f"Received MAC: {mac_hex.upper()}")

src/schnee/adapters/ntag/crypt.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from Crypto.Hash import CMAC
33
from Crypto.Util.Padding import pad
44

5+
SDM_SV2_PREFIX = bytes.fromhex("3CC300010080")
6+
57

68
def aes_encrypt(key: bytes, data: bytes, iv: bytes | None = None) -> bytes:
79
"""AES-128 CBC encryption"""
@@ -53,6 +55,28 @@ def calculate_ev2_mac( # noqa: PLR0913
5355
return bytes(list(full_mac)[1::2])
5456

5557

58+
def calculate_sdm_mac(
59+
sdm_key: bytes,
60+
signed_data: bytes,
61+
uid: bytes | None = None,
62+
counter: bytes | None = None,
63+
) -> bytes:
64+
"""Calculate the truncated 8-byte SDM MAC for NTAG 424 DNA."""
65+
sv2 = SDM_SV2_PREFIX
66+
if uid is not None:
67+
sv2 += uid
68+
if counter is not None:
69+
sv2 += counter
70+
71+
session_key_cmac = CMAC.new(key=sdm_key, ciphermod=AES)
72+
session_key_cmac.update(sv2)
73+
session_mac_key = session_key_cmac.digest()
74+
75+
sdm_mac_cmac = CMAC.new(key=session_mac_key, ciphermod=AES)
76+
sdm_mac_cmac.update(signed_data)
77+
return sdm_mac_cmac.digest()[1::2]
78+
79+
5680
def aes_cbc_encrypt_for_ev2(
5781
session_key_enc: bytes,
5882
plain_data: bytes,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Tests for NTAG cryptographic helpers."""
2+
3+
from Crypto.Cipher import AES
4+
from Crypto.Hash import CMAC
5+
6+
from schnee.adapters.ntag.crypt import SDM_SV2_PREFIX, calculate_sdm_mac
7+
8+
TRUNCATED_SDM_MAC_LENGTH = 8
9+
10+
11+
def test_calculate_sdm_mac_with_uid_and_counter() -> None:
12+
"""SDM MAC includes the UID and counter in SV2 when both are present."""
13+
assert (
14+
calculate_sdm_mac(
15+
sdm_key=bytes.fromhex("00112233445566778899AABBCCDDEEFF"),
16+
signed_data=bytes.fromhex("DEADBEEF00"),
17+
uid=bytes.fromhex("04782E21801D80"),
18+
counter=bytes.fromhex("010203"),
19+
).hex()
20+
== "7db100f509613111"
21+
)
22+
23+
24+
def test_calculate_sdm_mac_with_uid_only() -> None:
25+
"""SDM MAC includes only the UID when no counter is provided."""
26+
assert (
27+
calculate_sdm_mac(
28+
sdm_key=bytes.fromhex("00112233445566778899AABBCCDDEEFF"),
29+
signed_data=bytes.fromhex("DEADBEEF00"),
30+
uid=bytes.fromhex("04782E21801D80"),
31+
).hex()
32+
== "a8e8cc437f54250a"
33+
)
34+
35+
36+
def test_calculate_sdm_mac_without_uid_or_counter() -> None:
37+
"""SDM MAC can be derived from the fixed SV2 prefix alone."""
38+
assert (
39+
calculate_sdm_mac(
40+
sdm_key=bytes.fromhex("00112233445566778899AABBCCDDEEFF"),
41+
signed_data=bytes.fromhex("DEADBEEF00"),
42+
).hex()
43+
== "8330e2018ec638ce"
44+
)
45+
46+
47+
def test_calculate_sdm_mac_truncates_to_odd_indexed_bytes() -> None:
48+
"""SDM MAC returns the 8-byte [1::2] truncation of the full CMAC."""
49+
sdm_key = bytes.fromhex("00112233445566778899AABBCCDDEEFF")
50+
signed_data = bytes.fromhex("DEADBEEF00")
51+
uid = bytes.fromhex("04782E21801D80")
52+
counter = bytes.fromhex("010203")
53+
54+
session_key_cmac = CMAC.new(key=sdm_key, ciphermod=AES)
55+
session_key_cmac.update(SDM_SV2_PREFIX + uid + counter)
56+
session_mac_key = session_key_cmac.digest()
57+
58+
full_mac_cmac = CMAC.new(key=session_mac_key, ciphermod=AES)
59+
full_mac_cmac.update(signed_data)
60+
full_mac = full_mac_cmac.digest()
61+
62+
assert (
63+
calculate_sdm_mac(
64+
sdm_key=sdm_key,
65+
signed_data=signed_data,
66+
uid=uid,
67+
counter=counter,
68+
)
69+
== full_mac[1::2]
70+
)
71+
assert len(full_mac[1::2]) == TRUNCATED_SDM_MAC_LENGTH

0 commit comments

Comments
 (0)