diff --git a/chameleonultragui/lib/gui/component/mifare/classic.dart b/chameleonultragui/lib/gui/component/mifare/classic.dart index bd0c6e33a..a44718acd 100644 --- a/chameleonultragui/lib/gui/component/mifare/classic.dart +++ b/chameleonultragui/lib/gui/component/mifare/classic.dart @@ -66,6 +66,35 @@ class CardReaderState extends State { ); } else { var tags = appState.sharedPreferencesProvider.getCards(); + var dumpData = widget.mfcInfo.recovery!.cardData; + // Block 0 (the UID block) can't always be read. If it wasn't, ask before + // rebuilding it from the scanned UID so data is never changed silently. + if (!skipDump && !widget.mfcInfo.recovery!.block0Read) { + final rebuild = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(localizations.block0_read_failed_title), + content: Text(localizations.block0_read_failed_text), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(localizations.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(localizations.rebuild_uid), + ), + ], + ), + ); + if (rebuild == true) { + dumpData = List.of(dumpData); + dumpData[0] = mfClassicGenerateFirstBlock( + hexToBytes(widget.hfInfo.uid), + hexToBytes(widget.hfInfo.sak)[0], + hexToBytes(widget.hfInfo.atqa)); + } + } tags.add(CardSave( uid: widget.hfInfo.uid, sak: hexToBytes(widget.hfInfo.sak)[0], @@ -74,7 +103,7 @@ class CardReaderState extends State { tag: (skipDump) ? TagType.mifare1K : mfClassicGetChameleonTagType(widget.mfcInfo.type), - data: widget.mfcInfo.recovery!.cardData, + data: dumpData, ats: (widget.hfInfo.ats != localizations.no) ? hexToBytes(widget.hfInfo.ats) : Uint8List(0))); diff --git a/chameleonultragui/lib/helpers/mifare_classic/general.dart b/chameleonultragui/lib/helpers/mifare_classic/general.dart index 182867d9b..bd8fc66ee 100644 --- a/chameleonultragui/lib/helpers/mifare_classic/general.dart +++ b/chameleonultragui/lib/helpers/mifare_classic/general.dart @@ -577,13 +577,13 @@ Uint8List mfClassicGenerateFirstBlock(Uint8List uid, int sak, Uint8List atqa) { if (uid.length == 4) { block0.setAll(0, uid); block0[4] = calculateBcc(uid); - block0[5] = sak + 0x80; - block0.setAll(6, atqa); + block0[5] = sak; + block0.setAll(6, atqa.reversed); block0.setAll(8, [0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69]); } else if (uid.length == 7) { block0.setAll(0, uid); - block0[7] = sak + 0x80; - block0.setAll(8, atqa); + block0[7] = sak; + block0.setAll(8, atqa.reversed); block0.setAll(10, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]); } return block0; diff --git a/chameleonultragui/lib/helpers/mifare_classic/recovery.dart b/chameleonultragui/lib/helpers/mifare_classic/recovery.dart index ade56ea08..aa8fa32c1 100644 --- a/chameleonultragui/lib/helpers/mifare_classic/recovery.dart +++ b/chameleonultragui/lib/helpers/mifare_classic/recovery.dart @@ -43,6 +43,7 @@ class MifareClassicRecovery { void Function() update; MifareClassicType mifareClassicType; bool isMifareClassicEV1; + bool block0Read = false; MifareClassicRecovery( {required this.appState, @@ -570,6 +571,7 @@ class MifareClassicRecovery { Future dumpData() async { cardData = List.generate(256, (_) => Uint8List(0)); + block0Read = false; for (var sector = 0; sector < @@ -601,6 +603,8 @@ class MifareClassicRecovery { } else { continue; } + } else if (sector == 0 && block == 0) { + block0Read = true; } if (mfClassicGetSectorTrailerBlockBySector(sector) == diff --git a/chameleonultragui/lib/l10n/app_en.arb b/chameleonultragui/lib/l10n/app_en.arb index 2903f9e5f..966ea9093 100644 --- a/chameleonultragui/lib/l10n/app_en.arb +++ b/chameleonultragui/lib/l10n/app_en.arb @@ -570,5 +570,8 @@ "lf_sniff_load_file": "Load .bin file", "lf_sniff_load_failed": "Failed to load file: {error}", "lf_sniff_loaded": "Loaded {count} sample(s) from file.", - "sniff_device_required_hint": "No device connected. Connect a Chameleon to capture, or load a saved file." + "sniff_device_required_hint": "No device connected. Connect a Chameleon to capture, or load a saved file.", + "block0_read_failed_title": "Couldn't read block 0", + "block0_read_failed_text": "The UID block couldn't be read. Rebuild it from the scan so the clone keeps its UID? Cancel keeps the dump unchanged.", + "rebuild_uid": "Rebuild UID" } \ No newline at end of file diff --git a/chameleonultragui/test/mifare_classic_block0_test.dart b/chameleonultragui/test/mifare_classic_block0_test.dart new file mode 100644 index 000000000..d132128eb --- /dev/null +++ b/chameleonultragui/test/mifare_classic_block0_test.dart @@ -0,0 +1,29 @@ +import 'dart:typed_data'; + +import 'package:chameleonultragui/helpers/mifare_classic/general.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('mfClassicGenerateFirstBlock', () { + test('keeps SAK as-is and stores ATQA reversed (4-byte UID)', () { + // SAK 08 / ATQA 0004 must land as SAK 08 and ATQA bytes 04 00, + // matching the write-side block 0, not 88 00 04. + final block0 = mfClassicGenerateFirstBlock( + Uint8List.fromList([0x82, 0xB9, 0x4F, 0x4B]), + 0x08, + Uint8List.fromList([0x00, 0x04])); + expect(block0.length, 16); + expect(block0.sublist(0, 4), [0x82, 0xB9, 0x4F, 0x4B]); // UID + expect(block0[4], 0x3F); // BCC = 0x82 ^ 0xB9 ^ 0x4F ^ 0x4B + expect(block0[5], 0x08); // SAK, not 0x88 + expect(block0.sublist(6, 8), [0x04, 0x00]); // ATQA reversed + }); + + test('a genuine zero UID rebuilds to a zero UID', () { + final block0 = mfClassicGenerateFirstBlock( + Uint8List(4), 0x08, Uint8List.fromList([0x00, 0x04])); + expect(block0.sublist(0, 4), [0, 0, 0, 0]); + expect(block0[4], 0x00); // BCC of an all-zero UID + }); + }); +}