11from __future__ import annotations
22
3- from typing import Annotated
3+ from typing import Annotated , cast
44
55import typer
66
77from schnee .controllers .cli .errors import exit_for_service_error
88from schnee .controllers .cli .output import echo_json , echo_text
9+ from schnee .controllers .cli .parsing import parse_hex
10+ from schnee .ndef import NdefUriPrefix
911from schnee .services .backend import ListBackendNamesService
1012from schnee .services .base import ServiceError
1113from schnee .services .ntag_profile import (
1214 ReadNtagProfileService ,
15+ VerifyNtag424SdmMacService ,
1316 WriteNdefUrlService ,
1417)
1518from schnee .utils .ntag .constants import NtagByteLength
1619
1720
18- def _parse_optional_hex (value : str | None , * , option_name : str ) -> bytes | None :
19- """Convert an optional CLI hex value to bytes."""
20- if value is None :
21- return None
22- try :
23- parsed = bytes .fromhex (value )
24- except ValueError as exc :
25- msg = "must be valid hexadecimal"
26- raise typer .BadParameter (msg , param_hint = option_name ) from exc
27- if len (parsed ) != NtagByteLength .AES_KEY :
28- msg = "must be 32 hex characters that decode to 16 bytes"
29- raise typer .BadParameter (
30- msg ,
31- param_hint = option_name ,
32- )
33- return parsed
34-
35-
3621def register_commands (app : typer .Typer ) -> None :
3722 """Register CLI commands."""
38- ntag_app = typer .Typer (help = "NTAG profile commands." , no_args_is_help = True )
39- app .add_typer (ntag_app , name = "ntag" )
4023
4124 @app .callback (invoke_without_command = True )
4225 def root () -> None :
4326 """NFC/RFID tag authentication and encryption tools."""
4427
28+ _register_root_commands (app )
29+ _register_ntag_commands (app )
30+
31+
32+ def _register_root_commands (app : typer .Typer ) -> None :
33+ """Register root-level CLI commands."""
34+
4535 @app .command ("backends" )
4636 def backends () -> None :
4737 """List selectable backend names."""
@@ -53,6 +43,12 @@ def backends() -> None:
5343 for name in names :
5444 echo_text (name )
5545
46+
47+ def _register_ntag_commands (app : typer .Typer ) -> None :
48+ """Register NTAG CLI commands."""
49+ ntag_app = typer .Typer (help = "NTAG profile commands." , no_args_is_help = True )
50+ app .add_typer (ntag_app , name = "ntag" )
51+
5652 @ntag_app .command ("read" )
5753 def read_ntag_profile (
5854 backend_name : Annotated [
@@ -100,9 +96,10 @@ def write_ndef_url(
10096 ] = None ,
10197 ) -> None :
10298 """Write a URL NDEF record."""
103- ntag424_master_key = _parse_optional_hex (
99+ ntag424_master_key = parse_hex (
104100 ntag424_master_key_hex ,
105101 option_name = "--ntag424-master-key-hex" ,
102+ byte_length = NtagByteLength .AES_KEY ,
106103 )
107104 try :
108105 WriteNdefUrlService .call (
@@ -114,3 +111,98 @@ def write_ndef_url(
114111 )
115112 except ServiceError as exc :
116113 exit_for_service_error (exc )
114+
115+ @ntag_app .command ("verify-sdm-mac" )
116+ def verify_sdm_mac ( # noqa: PLR0913
117+ signed_text : Annotated [
118+ str ,
119+ typer .Option (
120+ "--signed-text" ,
121+ help = "Signed text from the mirrored URL." ,
122+ ),
123+ ],
124+ mac_hex : Annotated [
125+ str ,
126+ typer .Option (
127+ "--mac" ,
128+ help = "Observed 8-byte SDM MAC as 16 hex characters." ,
129+ ),
130+ ],
131+ sdm_key_hex : Annotated [
132+ str ,
133+ typer .Option (
134+ "--sdm-key-hex" ,
135+ help = "SDM file read key as 32 hex characters." ,
136+ ),
137+ ],
138+ ndef_prefix : Annotated [
139+ NdefUriPrefix ,
140+ typer .Option (
141+ "--ndef-prefix" ,
142+ help = "NDEF URI prefix token, for example no_prefix or https." ,
143+ ),
144+ ] = NdefUriPrefix .NO_PREFIX ,
145+ uid_hex : Annotated [
146+ str | None ,
147+ typer .Option (
148+ "--uid" ,
149+ help = "Optional 7-byte mirrored UID as 14 hex characters." ,
150+ ),
151+ ] = None ,
152+ counter_hex : Annotated [
153+ str | None ,
154+ typer .Option (
155+ "--counter" ,
156+ help = "Optional 3-byte mirrored read counter as 6 hex characters." ,
157+ ),
158+ ] = None ,
159+ ) -> None :
160+ """Verify an NTAG 424 DNA SDM MAC."""
161+ sdm_key = cast (
162+ "bytes" ,
163+ parse_hex (
164+ sdm_key_hex ,
165+ option_name = "--sdm-key-hex" ,
166+ byte_length = NtagByteLength .AES_KEY ,
167+ ),
168+ )
169+ mac = cast (
170+ "bytes" ,
171+ parse_hex (
172+ mac_hex ,
173+ option_name = "--mac" ,
174+ byte_length = NtagByteLength .SDM_MAC ,
175+ ),
176+ )
177+ uid = parse_hex (
178+ uid_hex ,
179+ option_name = "--uid" ,
180+ byte_length = NtagByteLength .UID ,
181+ )
182+ counter = parse_hex (
183+ counter_hex ,
184+ option_name = "--counter" ,
185+ byte_length = NtagByteLength .SDM_COUNTER ,
186+ )
187+ try :
188+ result = VerifyNtag424SdmMacService .call (
189+ VerifyNtag424SdmMacService .Request (
190+ signed_text = signed_text ,
191+ mac = mac ,
192+ sdm_key = sdm_key ,
193+ uid = uid ,
194+ counter = counter ,
195+ ndef_prefix = ndef_prefix ,
196+ ),
197+ )
198+ except ServiceError as exc :
199+ exit_for_service_error (exc )
200+
201+ echo_json (
202+ {
203+ "valid" : result .valid ,
204+ "calculated_mac" : result .calculated_mac .hex (),
205+ "ndef_prefix" : result .ndef_prefix .value ,
206+ "prefix_removed" : result .prefix_removed ,
207+ }
208+ )
0 commit comments