diff --git a/chameleonultragui/lib/bridge/chameleon.dart b/chameleonultragui/lib/bridge/chameleon.dart index 2d7c67ec6..cf1e093f7 100644 --- a/chameleonultragui/lib/bridge/chameleon.dart +++ b/chameleonultragui/lib/bridge/chameleon.dart @@ -1202,6 +1202,196 @@ class ChameleonCommunicator { .data; } + Future mf0UlcAuth(Uint8List key) async { + return (await sendCmd(ChameleonCommand.mf0UlcAuth, + data: Uint8List.fromList([...key])))! + .status == + 0; + } + + Future mf0UlcReadPages(Uint8List key, int page, int count) async { + return (await sendCmd(ChameleonCommand.mf0UlcRead, + data: Uint8List.fromList([...key, page, count])))! + .data; + } + + Future mf0UlcWritePage(Uint8List key, int page, Uint8List data) async { + return (await sendCmd(ChameleonCommand.mf0UlcWrite, + data: Uint8List.fromList([...key, page, ...data])))! + .status == + 0; + } + + Future mf0UlcSetKey(Uint8List oldKey, Uint8List newKey) async { + return (await sendCmd(ChameleonCommand.mf0UlcSetKey, + data: Uint8List.fromList([...oldKey, ...newKey])))! + .status == + 0; + } + + Future hf14aSetFieldOn() async { + await sendCmd(ChameleonCommand.hf14aSetFieldOn); + } + + Future hf14aSetFieldOff() async { + await sendCmd(ChameleonCommand.hf14aSetFieldOff); + } + + Future mf1GetFieldOffDoReset() async { + return (await sendCmd(ChameleonCommand.mf1GetFieldOffDoReset))!.data[0] == 1; + } + + Future mf1SetFieldOffDoReset(bool enabled) async { + await sendCmd(ChameleonCommand.mf1SetFieldOffDoReset, + data: Uint8List.fromList([enabled ? 1 : 0])); + } + + Future deleteSlotTagNick(int index, TagFrequency frequency) async { + await sendCmd(ChameleonCommand.deleteSlotTagNick, + data: Uint8List.fromList([index, frequency.value])); + } + + Future> scan14443aTagKeep() async { + var resp = await sendCmd(ChameleonCommand.hf14aScanKeep); + List cards = []; + if (resp != null && resp.data.isNotEmpty) { + int offset = 0; + while (offset < resp.data.length) { + int uidLength = resp.data[offset]; + int atsLength = resp.data[offset + uidLength + 4]; + cards.add(CardData( + uid: resp.data.sublist(offset + 1, offset + uidLength + 1), + atqa: Uint8List.fromList(resp.data + .sublist(offset + uidLength + 1, offset + uidLength + 3) + .reversed + .toList()), + sak: resp.data[offset + uidLength + 3], + ats: resp.data.sublist( + offset + uidLength + 5, offset + uidLength + 5 + atsLength), + )); + offset += uidLength + 5 + atsLength; + } + } + return cards; + } + + Future hf14aAuthTrace(int block, int keyType, Uint8List key, + {int timeoutMs = 5000}) async { + return (await sendCmd(ChameleonCommand.hf14aAuthTrace, + data: Uint8List.fromList( + [keyType, block, ...key, (timeoutMs >> 8) & 0xFF, timeoutMs & 0xFF]), + timeout: Duration(milliseconds: timeoutMs + 3000)))! + .data; + } + + Future<(int, int, int, int)> hf14aGetConfig() async { + var data = (await sendCmd(ChameleonCommand.hf14aGetConfig))!.data; + int signed(int b) => b > 127 ? b - 256 : b; + return (signed(data[0]), signed(data[1]), signed(data[2]), signed(data[3])); + } + + Future hf14aSetConfig(int bcc, int cl2, int cl3, int rats) async { + await sendCmd(ChameleonCommand.hf14aSetConfig, + data: Uint8List.fromList( + [bcc & 0xFF, cl2 & 0xFF, cl3 & 0xFF, rats & 0xFF])); + } + + Future ioProxDecodeRaw(Uint8List raw8) async { + return (await sendCmd(ChameleonCommand.ioProxDecodeRaw, + data: Uint8List.fromList([...raw8])))! + .data; + } + + Future ioProxComposeID(int version, int facility, int card) async { + return (await sendCmd(ChameleonCommand.ioProxComposeID, + data: Uint8List.fromList( + [version, facility, (card >> 8) & 0xFF, card & 0xFF])))! + .data; + } + + Future adcGenericRead() async { + return (await sendCmd(ChameleonCommand.adcGenericRead))!.data; + } + + Future writeT55XX(int block, Uint8List word, + {Uint8List? password, bool page1 = false}) async { + bool usePwd = password != null; + Uint8List pwd = password ?? Uint8List(4); + await sendCmd(ChameleonCommand.writeT55XX, + data: Uint8List.fromList( + [block, ...word, usePwd ? 1 : 0, ...pwd, page1 ? 1 : 0])); + } + + Future<(int, int, int, bool, int)> scanEM4X05({int password = 0}) async { + var data = (await sendCmd(ChameleonCommand.scanEM4X05Tag, + data: Uint8List.fromList([ + (password >> 24) & 0xFF, + (password >> 16) & 0xFF, + (password >> 8) & 0xFF, + password & 0xFF + ])))! + .data; + return ( + bytesToU32(data.sublist(0, 4)), + bytesToU32(data.sublist(4, 8)), + bytesToU32(data.sublist(8, 12)), + data[12] == 1, + data[13] + ); + } + + Future<(int, Uint8List)> hf14a4ApduRecv() async { + var resp = (await sendCmd(ChameleonCommand.hf14a4ApduRecv, + timeout: const Duration(seconds: 2)))!; + return (resp.status, resp.data); + } + + Future hf14a4ApduSend(Uint8List response) async { + await sendCmd(ChameleonCommand.hf14a4ApduSend, + data: Uint8List.fromList([ + (response.length >> 8) & 0xFF, + response.length & 0xFF, + ...response + ])); + } + + Future hf14a4SetAntiColl( + Uint8List uid, Uint8List atqa, int sak, Uint8List ats) async { + await sendCmd(ChameleonCommand.hf14a4SetAntiColl, + data: Uint8List.fromList( + [uid.length, ...uid, ...atqa, sak, ats.length, ...ats])); + } + + Future hf14a4AddStaticResponse( + Uint8List cmd, Uint8List response) async { + await sendCmd(ChameleonCommand.hf14a4StaticResp, + data: Uint8List.fromList([ + cmd.length, + ...cmd, + (response.length >> 8) & 0xFF, + response.length & 0xFF, + ...response + ])); + } + + Future hf14a4ClearStaticResponses() async { + await sendCmd(ChameleonCommand.hf14a4StaticResp, + data: Uint8List.fromList([0])); + } + + Future hf14a4ReaderApdu(Uint8List apdu) async { + return (await sendCmd(ChameleonCommand.hf14a4ReaderApdu, + data: Uint8List.fromList([...apdu]), + timeout: const Duration(seconds: 3)))! + .data; + } + + Future hf14a4EmvScan() async { + return (await sendCmd(ChameleonCommand.hf14a4EmvScan, + timeout: const Duration(seconds: 10)))! + .data; + } + Future mf0GetMagicMode() async { return (await sendCmd(ChameleonCommand.mf0NtagGetUidMagicMode))!.data[0] == 1; diff --git a/chameleonultragui/lib/gui/component/mifare/ultralight.dart b/chameleonultragui/lib/gui/component/mifare/ultralight.dart index 357a25c44..27fb7e597 100644 --- a/chameleonultragui/lib/gui/component/mifare/ultralight.dart +++ b/chameleonultragui/lib/gui/component/mifare/ultralight.dart @@ -1,6 +1,7 @@ import 'package:chameleonultragui/gui/component/card_button.dart'; import 'package:chameleonultragui/gui/component/error_message.dart'; import 'package:chameleonultragui/gui/page/read_card.dart'; +import 'package:chameleonultragui/helpers/definitions.dart'; import 'package:chameleonultragui/helpers/general.dart'; import 'package:chameleonultragui/helpers/mifare_ultralight/general.dart'; import 'package:chameleonultragui/helpers/validators.dart'; @@ -40,6 +41,89 @@ class CardReaderState extends State { String error = ""; double progress = -1; + static const int ulcReadablePages = 0x2C; + + bool get isUlc => widget.hfInfo.type == TagType.ultralightC; + + Future readUlcCard({bool withKey = true}) async { + var appState = Provider.of(context, listen: false); + var localizations = AppLocalizations.of(context)!; + + setState(() { + cardData = []; + error = ""; + progress = -1; + state = MifareUltralightState.read; + }); + + if (withKey) { + Uint8List key = hexToBytes(keyController.text); + + Uint8List data; + try { + data = await appState.communicator! + .mf0UlcReadPages(key, 0, ulcReadablePages); + } catch (_) { + data = Uint8List(0); + } + + if (data.length < 4) { + setState(() { + progress = 0; + cardData = []; + error = localizations.invalid_password; + state = MifareUltralightState.none; + }); + return; + } + + int pagesRead = data.length ~/ 4; + for (int page = 0; page < pagesRead; page++) { + cardData.add(Uint8List.fromList(data.sublist(page * 4, page * 4 + 4))); + } + + if (cardData.length == ulcReadablePages) { + Uint8List cardKey = mfUltralightSwapUlcKeyOrder(key); + for (int i = 0; i < 4; i++) { + cardData.add(Uint8List.fromList(cardKey.sublist(i * 4, i * 4 + 4))); + } + } + } else { + for (int page = 0; page < ulcReadablePages; page++) { + Uint8List pageData = await appState.communicator! + .send14ARaw(Uint8List.fromList([0x30, page])); + if (pageData.isNotEmpty) { + cardData.add(Uint8List.fromList(pageData.slice(0, 4).toList())); + } else { + cardData.add(Uint8List(0)); + } + setState(() { + progress = page / ulcReadablePages; + }); + } + + if (!cardData.any((block) => block.isNotEmpty)) { + setState(() { + progress = 0; + cardData = []; + error = localizations.failed_to_read_block; + state = MifareUltralightState.none; + }); + return; + } + + for (int i = 0; i < 4; i++) { + cardData.add(Uint8List(4)); + } + } + + setState(() { + progress = 1; + error = ""; + state = MifareUltralightState.save; + }); + } + Future readCard({bool withPassword = false}) async { var appState = Provider.of(context, listen: false); var localizations = AppLocalizations.of(context)!; @@ -127,9 +211,7 @@ class CardReaderState extends State { List cardDump = []; var localizations = AppLocalizations.of(context)!; - for (var page = 0; - page < mfUltralightGetPagesCount(widget.hfInfo.type); - page++) { + for (var page = 0; page < cardData.length; page++) { if (cardData[page].isEmpty) { cardDump.addAll(Uint8List(4)); } else { @@ -181,35 +263,57 @@ class CardReaderState extends State { decoration: InputDecoration( labelText: localizations.key, hintMaxLines: 4, - hintText: localizations - .enter_something(localizations.ultralight_key_prompt)), + hintText: isUlc + ? "" + : localizations.enter_something( + localizations.ultralight_key_prompt)), inputFormatters: hexFormatter, validator: (value) => validateHex(value, localizations, - exactBytes: 4, fieldName: localizations.key), + exactBytes: isUlc ? 16 : 4, fieldName: localizations.key), ), ), const SizedBox(height: 8), - Row(children: [ - Expanded( - child: TextButton( - onPressed: () async => {await readCard(withPassword: true)}, - child: Text(localizations.read_with_key), + if (isUlc) + Row(children: [ + Expanded( + child: TextButton( + onPressed: () async { + if (formKey.currentState!.validate()) { + await readUlcCard(withKey: true); + } + }, + child: Text(localizations.read_with_key), + ), ), - ), - Expanded( - child: TextButton( - onPressed: () async => {await readCard(withPassword: false)}, - child: Text(localizations.read_without_key), + Expanded( + child: TextButton( + onPressed: () async => {await readUlcCard(withKey: false)}, + child: Text(localizations.read_without_key), + ), ), - ), - ]), + ]) + else + Row(children: [ + Expanded( + child: TextButton( + onPressed: () async => {await readCard(withPassword: true)}, + child: Text(localizations.read_with_key), + ), + ), + Expanded( + child: TextButton( + onPressed: () async => {await readCard(withPassword: false)}, + child: Text(localizations.read_without_key), + ), + ), + ]), ], if (error != "") ...[ const SizedBox(height: 16), ErrorMessage(errorMessage: error), ], if (state == MifareUltralightState.read) ...[ - LinearProgressIndicator(value: progress), + LinearProgressIndicator(value: progress < 0 ? null : progress), const SizedBox(height: 8) ], if (state == MifareUltralightState.save) diff --git a/chameleonultragui/lib/gui/menu/dialogs/slot/edit.dart b/chameleonultragui/lib/gui/menu/dialogs/slot/edit.dart index c945becfe..2cefe969d 100644 --- a/chameleonultragui/lib/gui/menu/dialogs/slot/edit.dart +++ b/chameleonultragui/lib/gui/menu/dialogs/slot/edit.dart @@ -225,8 +225,8 @@ class SlotEditMenuState extends State { .setHIDProxEmulatorID(hexToBytes(hidCard.toString())); } catch (_) {} } else if (selectedType! == TagType.pac) { - await appState.communicator!.setPacEmulatorID( - hexToBytes(uidController.text.replaceAll(' ', ''))); + await appState.communicator! + .setPacEmulatorID(hexToBytes(uidController.text.replaceAll(' ', ''))); } else if (selectedType! == TagType.ioProx) { await appState.communicator!.setIoProxEmulatorID( hexToBytes(uidController.text.replaceAll(' ', ''))); @@ -685,129 +685,127 @@ class SlotEditMenuState extends State { ? true : false); }), - const SizedBox(height: 8), - Text( - localizations.password_detection), - const SizedBox(height: 8), - ToggleButtonsWrapper( - items: [ - localizations.yes, - localizations.no - ], - selectedValue: emulatorSettings! - .isDetectionEnabled - ? 0 - : 1, - onChange: (int index) async { - await appState.communicator! - .mf0NtagSetDetectionEnable( - index == 0 - ? true - : false); - }), - ...(emulatorSettings! - .isDetectionEnabled) - ? [ - ...(detectionCount == 0) - ? [ - const SizedBox( - height: 8), - Text( - localizations - .present_cham_reader_keys, - textScaler: - const TextScaler - .linear( - 0.8)) - ] - : [ - const SizedBox( - height: 8), - Text( - '${localizations.passwords_detected}: $detectionCount', - textScaler: - const TextScaler - .linear( - 0.9)), - const SizedBox( - height: 8), - Row( - mainAxisAlignment: - MainAxisAlignment - .center, - children: [ - TextButton( - onPressed: - () async { - List - passwords = - await appState - .communicator! - .mf0NtagGetDetectionLog(0); - - if (!context - .mounted) { - return; - } - - showDialog( - context: - context, - builder: - (BuildContext - context) { - TextEditingController - passwordController = - TextEditingController(); - passwordController.text = passwords - .join('\n') - .toUpperCase(); - - return AlertDialog( - title: - Text(localizations.detected_passwords), - content: - SizedBox( - width: double.maxFinite, - child: TextFormField( - maxLines: null, - controller: passwordController, - readOnly: true, - style: const TextStyle(fontFamily: 'RobotoMono', fontSize: 16.0), - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(localizations.close), - ), - ], - ); - }, - ); - }, - child: Row( - children: [ - const Icon( - Icons.visibility), - Text(localizations - .view_passwords), - ], - )), - ]), - ], - ] - : [ - const SizedBox(height: 8), - Text( - localizations - .enable_password_detection, - textScaler: - const TextScaler - .linear(0.8)) + if (selectedType != + TagType.ultralightC) ...[ + const SizedBox(height: 8), + Text(localizations + .password_detection), + const SizedBox(height: 8), + ToggleButtonsWrapper( + items: [ + localizations.yes, + localizations.no ], + selectedValue: emulatorSettings! + .isDetectionEnabled + ? 0 + : 1, + onChange: (int index) async { + await appState.communicator! + .mf0NtagSetDetectionEnable( + index == 0 + ? true + : false); + }), + ...(emulatorSettings! + .isDetectionEnabled) + ? [ + ...(detectionCount == 0) + ? [ + const SizedBox( + height: 8), + Text( + localizations + .present_cham_reader_keys, + textScaler: + const TextScaler + .linear( + 0.8)) + ] + : [ + const SizedBox( + height: 8), + Text( + '${localizations.passwords_detected}: $detectionCount', + textScaler: + const TextScaler + .linear( + 0.9)), + const SizedBox( + height: 8), + Row( + mainAxisAlignment: + MainAxisAlignment + .center, + children: [ + TextButton( + onPressed: + () async { + List + passwords = + await appState.communicator!.mf0NtagGetDetectionLog(0); + + if (!context + .mounted) { + return; + } + + showDialog( + context: + context, + builder: + (BuildContext context) { + TextEditingController + passwordController = + TextEditingController(); + passwordController.text = + passwords.join('\n').toUpperCase(); + + return AlertDialog( + title: Text(localizations.detected_passwords), + content: SizedBox( + width: double.maxFinite, + child: TextFormField( + maxLines: null, + controller: passwordController, + readOnly: true, + style: const TextStyle(fontFamily: 'RobotoMono', fontSize: 16.0), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(localizations.close), + ), + ], + ); + }, + ); + }, + child: + Row( + children: [ + const Icon( + Icons.visibility), + Text(localizations + .view_passwords), + ], + )), + ]), + ], + ] + : [ + const SizedBox(height: 8), + Text( + localizations + .enable_password_detection, + textScaler: + const TextScaler + .linear(0.8)) + ], + ], const SizedBox(height: 8), Text(localizations.write_mode), const SizedBox(height: 8), diff --git a/chameleonultragui/lib/gui/menu/dialogs/slot/export.dart b/chameleonultragui/lib/gui/menu/dialogs/slot/export.dart index 2d49fde3a..32c172fbc 100644 --- a/chameleonultragui/lib/gui/menu/dialogs/slot/export.dart +++ b/chameleonultragui/lib/gui/menu/dialogs/slot/export.dart @@ -3,8 +3,7 @@ import 'dart:convert'; import 'package:chameleonultragui/gui/component/card_list.dart'; import 'package:chameleonultragui/gui/component/toggle_buttons.dart'; import 'package:chameleonultragui/helpers/definitions.dart'; -import 'package:chameleonultragui/helpers/mifare_classic/general.dart'; -import 'package:chameleonultragui/helpers/mifare_ultralight/general.dart'; +import 'package:chameleonultragui/helpers/slot_dump.dart'; import 'package:flutter/material.dart'; import 'package:chameleonultragui/helpers/general.dart'; import 'package:chameleonultragui/sharedprefsprovider.dart'; @@ -76,101 +75,8 @@ class SlotExportMenuState extends State { ); } } else { - CardData data = await appState.communicator!.mf1GetAntiCollData(); - - if (isMifareUltralight(widget.slotTypes.hf)) { - int pageCount = mfUltralightGetPagesCount(widget.slotTypes.hf); - List pages = []; - - for (int page = 0; page < pageCount; page++) { - Uint8List pageData = - await appState.communicator!.mf0EmulatorReadPages(page, 1); - pages.add(pageData); - } - - CardSaveExtra extraData = CardSaveExtra(); - - Uint8List version = - await appState.communicator!.mf0EmulatorGetVersionData(); - if (version.isNotEmpty) { - extraData.ultralightVersion = version; - } - - Uint8List signature = - await appState.communicator!.mf0EmulatorGetSignatureData(); - if (signature.isNotEmpty) { - extraData.ultralightSignature = signature; - } - - if (mfUltralightHasCounters(widget.slotTypes.hf)) { - List counters = []; - int counterCount = mfUltralightGetCounterCount(widget.slotTypes.hf); - - for (int i = 0; i < counterCount; i++) { - var counterData = - await appState.communicator!.mf0EmulatorGetCounterData(i); - counters.add(counterData.$1); - } - - if (counters.isNotEmpty) { - extraData.ultralightCounters = counters; - } - } - - return CardSave( - uid: bytesToHexSpace(data.uid), - name: widget.names.hf, - sak: data.sak, - atqa: data.atqa, - ats: data.ats, - tag: widget.slotTypes.hf, - data: pages, - extraData: extraData, - ); - } else { - int blockCount = mfClassicGetBlockCount( - chameleonTagTypeGetMfClassicType(widget.slotTypes.hf)); - - Uint8List binData = Uint8List(blockCount * 16); - - int readCount = 16; - int binDataIndex = 0; - - for (int currentBlock = 0; - currentBlock < blockCount; - currentBlock += readCount) { - Uint8List result = await appState.communicator! - .mf1GetEmulatorBlock(currentBlock, readCount); - - binData.setAll(binDataIndex, result); - binDataIndex += result.length; - } - - int remainingBlocks = blockCount % readCount; - - if (remainingBlocks != 0) { - Uint8List result = await appState.communicator!.mf1GetEmulatorBlock( - blockCount - remainingBlocks, remainingBlocks); - binData.setAll(binDataIndex, result); - } - - List blocks = []; - - for (int i = 0; i < binData.length; i += 16) { - Uint8List block = Uint8List.fromList(binData.sublist(i, i + 16)); - blocks.add(block); - } - - return CardSave( - uid: bytesToHexSpace(data.uid), - name: widget.names.hf, - sak: data.sak, - atqa: data.atqa, - ats: data.ats, - tag: widget.slotTypes.hf, - data: blocks, - ); - } + return await readHfDumpFromSlot( + appState.communicator!, widget.names.hf, widget.slotTypes.hf); } return null; diff --git a/chameleonultragui/lib/gui/menu/dialogs/slot/settings.dart b/chameleonultragui/lib/gui/menu/dialogs/slot/settings.dart index b25db775b..4d0bfbc20 100644 --- a/chameleonultragui/lib/gui/menu/dialogs/slot/settings.dart +++ b/chameleonultragui/lib/gui/menu/dialogs/slot/settings.dart @@ -1,7 +1,14 @@ +import 'dart:typed_data'; + import 'package:chameleonultragui/gui/component/error_page.dart'; import 'package:chameleonultragui/gui/menu/dialogs/slot/edit.dart'; import 'package:chameleonultragui/gui/menu/dialogs/slot/export.dart'; +import 'package:chameleonultragui/gui/menu/pages/dump_editor.dart'; import 'package:chameleonultragui/helpers/definitions.dart'; +import 'package:chameleonultragui/helpers/mifare_classic/general.dart'; +import 'package:chameleonultragui/helpers/mifare_ultralight/general.dart'; +import 'package:chameleonultragui/helpers/slot_dump.dart'; +import 'package:chameleonultragui/sharedprefsprovider.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:chameleonultragui/main.dart'; @@ -79,6 +86,91 @@ class SlotSettingsState extends State { setState(() {}); } + Future openHfDumpEditor() async { + var appState = context.read(); + var localizations = AppLocalizations.of(context)!; + var navigator = Navigator.of(context); + + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => AlertDialog( + content: Row( + children: [ + const CircularProgressIndicator(), + const SizedBox(width: 16), + Expanded(child: Text(localizations.please_wait)), + ], + ), + ), + ); + + CardSave card; + try { + await appState.communicator!.activateSlot(widget.slot); + card = await readHfDumpFromSlot( + appState.communicator!, names.hf, slotTypes.hf); + } catch (e) { + appState.log!.e("Failed to read slot dump: $e"); + navigator.pop(); + return; + } + + navigator.pop(); + + if (!mounted) { + return; + } + + List? editedDump; + await Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => DumpEditor( + cardSave: card, + onSave: (dumpData) { + editedDump = dumpData; + }, + ), + ), + ); + + if (editedDump == null || !mounted) { + return; + } + + ValueNotifier progress = ValueNotifier(0); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) => AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(localizations.uploading_dump), + const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: progress, + builder: (context, value, _) => + LinearProgressIndicator(value: value), + ), + ], + ), + ), + ); + + try { + await writeHfDumpToSlot( + appState.communicator!, widget.slot, slotTypes.hf, editedDump!, + onProgress: (p) => progress.value = p / 100); + } catch (e) { + appState.log!.e("Failed to write slot dump: $e"); + } finally { + navigator.pop(); + progress.dispose(); + } + } + @override Widget build(BuildContext context) { var appState = context.watch(); @@ -146,6 +238,13 @@ class SlotSettingsState extends State { }, icon: const Icon(Icons.edit), ), + IconButton( + onPressed: (isMifareClassic(slotTypes.hf) || + isMifareUltralight(slotTypes.hf)) + ? openHfDumpEditor + : null, + icon: const Icon(Icons.edit_document), + ), IconButton( onPressed: () async { if (appState.sharedPreferencesProvider diff --git a/chameleonultragui/lib/gui/page/home.dart b/chameleonultragui/lib/gui/page/home.dart index 10693c931..9585433d3 100644 --- a/chameleonultragui/lib/gui/page/home.dart +++ b/chameleonultragui/lib/gui/page/home.dart @@ -53,7 +53,7 @@ class HomePageState extends State { // Checks that firmware supports all functions of current app // If not, prompt user to update firmware (as outdated firmware might break app) - int ultraCapability = ChameleonCommand.setIdteckEmulatorID.value; + int ultraCapability = ChameleonCommand.mf0UlcAuth.value; int liteCapability = ChameleonCommand.setIdteckEmulatorID.value; var appState = context.read(); diff --git a/chameleonultragui/lib/helpers/definitions.dart b/chameleonultragui/lib/helpers/definitions.dart index d65a12ff9..d1de3cf50 100644 --- a/chameleonultragui/lib/helpers/definitions.dart +++ b/chameleonultragui/lib/helpers/definitions.dart @@ -22,6 +22,7 @@ enum ChameleonCommand { getSlotInfo(1019), getEnabledSlots(1023), deleteSlotInfo(1024), + deleteSlotTagNick(1021), getAllSlotNicks(1038), // bootloader @@ -76,7 +77,25 @@ enum ChameleonCommand { mf1StaticEncryptedNestedAcquire(2014), mf1CheckKeysOnBlock(2015), hf14ARawCommand(2010), + hf14aScanKeep(2016), + hf14aAuthTrace(2017), + mf0UlcAuth(2018), + mf0UlcRead(2019), + mf0UlcWrite(2021), + mf0UlcSetKey(2022), hf14aSniff(2020), + hf14aSetFieldOn(2100), + hf14aSetFieldOff(2101), + hf14aGetConfig(2200), + hf14aSetConfig(2201), + + // hf 14a-4 (T=CL) commands + hf14a4ApduRecv(6000), + hf14a4ApduSend(6001), + hf14a4SetAntiColl(6002), + hf14a4StaticResp(6003), + hf14a4ReaderApdu(6004), + hf14a4EmvScan(6005), // lf commands scanEM410Xtag(3000), @@ -91,6 +110,11 @@ enum ChameleonCommand { scanPacTag(3014), writePacToT5577(3015), writeIdteckToT5577(3018), + ioProxDecodeRaw(3012), + ioProxComposeID(3013), + adcGenericRead(3009), + writeT55XX(3016), + scanEM4X05Tag(3030), lfSniff(3031), mf1LoadBlockData(4000), @@ -134,6 +158,8 @@ enum ChameleonCommand { mf0NtagGetEmulatorConfig(4037), mf1GetPrngType(4040), mf1SetPrngType(4041), + mf1SetFieldOffDoReset(4038), + mf1GetFieldOffDoReset(4039), // read slot info mf1GetBlockData(4008), diff --git a/chameleonultragui/lib/helpers/mifare_ultralight/dump_highlighter.dart b/chameleonultragui/lib/helpers/mifare_ultralight/dump_highlighter.dart index 7ec4cb4ff..1db1ca3a5 100644 --- a/chameleonultragui/lib/helpers/mifare_ultralight/dump_highlighter.dart +++ b/chameleonultragui/lib/helpers/mifare_ultralight/dump_highlighter.dart @@ -39,6 +39,8 @@ class MifareUltralightDumpHighlighter { return highlightUidBlock(blockData, context); } else if (blockNumber == 2) { return highlightBccBlock(blockData, context); + } else if (mfUltralightGetKeyPages(cardSave.tag).contains(blockNumber)) { + return highlightPasswordBlock(blockData, context); } else if (blockNumber == mfUltralightGetPasswordPage(cardSave.tag)) { return highlightPasswordBlock(blockData, context); } else { diff --git a/chameleonultragui/lib/helpers/mifare_ultralight/general.dart b/chameleonultragui/lib/helpers/mifare_ultralight/general.dart index e613391d3..629bd1c6c 100644 --- a/chameleonultragui/lib/helpers/mifare_ultralight/general.dart +++ b/chameleonultragui/lib/helpers/mifare_ultralight/general.dart @@ -101,6 +101,25 @@ int mfUltralightGetPasswordPage(TagType type) { return 0; } +List mfUltralightGetKeyPages(TagType type) { + if (type == TagType.ultralightC) { + return [44, 45, 46, 47]; + } + return []; +} + +Uint8List mfUltralightSwapUlcKeyOrder(Uint8List key) { + if (key.length != 16) { + return Uint8List.fromList(key); + } + Uint8List out = Uint8List(16); + for (int i = 0; i < 8; i++) { + out[i] = key[7 - i]; + out[8 + i] = key[15 - i]; + } + return out; +} + bool mfUltralightHasCounters(TagType type) { return [ TagType.ultralight11, @@ -232,14 +251,28 @@ List mfUltralightGenerateFirstBlocks(Uint8List uid, TagType type) { return blocks; } +Future mfUltralightIsUlc(ChameleonCommunicator communicator) async { + try { + Uint8List resp = + await communicator.send14ARaw(Uint8List.fromList([0x1A, 0x00])); + return resp.length >= 9 && resp[0] == 0xAF; + } catch (_) { + return false; + } +} + Future<(TagType, MifareUltralightInfo)> performMifareUltralightScan( ChameleonCommunicator communicator, MifareUltralightInfo mfuInfo, {TagType? override}) async { - TagType type = TagType.unknown; + TagType type = override ?? TagType.unknown; Uint8List version = await mfUltralightGetVersion(communicator); - if (version.isNotEmpty) { - type = override ?? mfUltralightGetType(version); + if (type == TagType.unknown && version.isNotEmpty) { + type = mfUltralightGetType(version); + } + + if (type == TagType.unknown && await mfUltralightIsUlc(communicator)) { + type = TagType.ultralightC; } if (type == TagType.unknown) { diff --git a/chameleonultragui/lib/helpers/mifare_ultralight/write/base.dart b/chameleonultragui/lib/helpers/mifare_ultralight/write/base.dart index d5bf1bfdd..01b9d02e9 100644 --- a/chameleonultragui/lib/helpers/mifare_ultralight/write/base.dart +++ b/chameleonultragui/lib/helpers/mifare_ultralight/write/base.dart @@ -1,5 +1,7 @@ import 'package:chameleonultragui/gui/page/read_card.dart'; +import 'package:chameleonultragui/helpers/definitions.dart'; import 'package:chameleonultragui/helpers/general.dart'; +import 'package:chameleonultragui/helpers/mifare_ultralight/general.dart'; import 'package:chameleonultragui/helpers/validators.dart'; import 'package:chameleonultragui/helpers/write.dart'; import 'package:chameleonultragui/sharedprefsprovider.dart'; @@ -22,19 +24,22 @@ class BaseMifareUltralightWriteHelper extends AbstractWriteHelper { static String get staticName => "gen2"; TextEditingController keyController = TextEditingController(); String? key; + TagType? tagType; - BaseMifareUltralightWriteHelper(super.communicator); + bool get isUlc => tagType == TagType.ultralightC; + + BaseMifareUltralightWriteHelper(super.communicator, {this.tagType}); @override List getAvailableMethods() { return [ - BaseMifareUltralightWriteHelper(communicator), + BaseMifareUltralightWriteHelper(communicator, tagType: tagType), ]; } @override List getAvailableMethodsByPriority() { - return [BaseMifareUltralightWriteHelper(communicator)]; + return [BaseMifareUltralightWriteHelper(communicator, tagType: tagType)]; } @override @@ -42,6 +47,42 @@ class BaseMifareUltralightWriteHelper extends AbstractWriteHelper { var localizations = AppLocalizations.of(context)!; final GlobalKey formKey = GlobalKey(); + if (isUlc) { + return Row(children: [ + Expanded( + child: Form( + key: formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Column( + children: [ + TextFormField( + controller: keyController, + decoration: InputDecoration( + labelText: localizations.key, + hintMaxLines: 4, + hintText: + localizations.enter_something(localizations.key)), + inputFormatters: hexFormatter, + validator: (value) => validateHex(value, localizations, + exactBytes: 16, + fieldName: localizations.key, + required: true), + ) + ], + ))), + TextButton( + onPressed: () { + if (formKey.currentState!.validate()) { + setState(() { + key = keyController.text; + }); + } + }, + child: Text(localizations.next), + ), + ]); + } + return Row(children: [ Expanded( child: Form( @@ -107,9 +148,77 @@ class BaseMifareUltralightWriteHelper extends AbstractWriteHelper { key = null; } + Uint8List? _ulcDumpKey(CardSave card) { + const int firstKeyPage = 0x2C; + if (card.data.length <= firstKeyPage + 3) { + return null; + } + + List stored = []; + for (int page = firstKeyPage; page <= firstKeyPage + 3; page++) { + if (card.data[page].length != 4) { + return null; + } + stored.addAll(card.data[page]); + } + + if (stored.every((byte) => byte == 0)) { + return null; + } + + return Uint8List.fromList(stored); + } + + Future writeUlcData( + CardSave card, Function(int writeProgress) update) async { + failedBlocks = []; + + if (!await communicator.isReaderDeviceMode()) { + await communicator.setReaderDeviceMode(true); + } + + if (await communicator.scan14443aTag() == null) { + return false; + } + + Uint8List ulcKey = hexToBytes(key ?? ""); + if (ulcKey.length != 16 || !await communicator.mf0UlcAuth(ulcKey)) { + return false; + } + + const int firstPage = 0x04; + const int lastPage = 0x27; + + for (int page = firstPage; page <= lastPage; page++) { + if (page < card.data.length && card.data[page].length == 4) { + if (!await communicator.mf0UlcWritePage( + ulcKey, page, card.data[page])) { + failedBlocks.add(page); + } + } + + update( + ((page - firstPage + 1) / (lastPage - firstPage + 1) * 100).round()); + } + + Uint8List? dumpKeyCardOrder = _ulcDumpKey(card); + if (dumpKeyCardOrder != null) { + Uint8List newKey = mfUltralightSwapUlcKeyOrder(dumpKeyCardOrder); + if (!await communicator.mf0UlcSetKey(ulcKey, newKey)) { + failedBlocks.add(0x2C); + } + } + + return failedBlocks.isEmpty; + } + @override Future writeData( CardSave card, Function(int writeProgress) update) async { + if (isUlc) { + return writeUlcData(card, update); + } + int totalBlocks = card.data.length; if (!await communicator.isReaderDeviceMode()) { diff --git a/chameleonultragui/lib/helpers/slot_dump.dart b/chameleonultragui/lib/helpers/slot_dump.dart new file mode 100644 index 000000000..75367e855 --- /dev/null +++ b/chameleonultragui/lib/helpers/slot_dump.dart @@ -0,0 +1,151 @@ +import 'dart:typed_data'; + +import 'package:chameleonultragui/bridge/chameleon.dart'; +import 'package:chameleonultragui/helpers/definitions.dart'; +import 'package:chameleonultragui/helpers/general.dart'; +import 'package:chameleonultragui/helpers/mifare_classic/general.dart'; +import 'package:chameleonultragui/helpers/mifare_ultralight/general.dart'; +import 'package:chameleonultragui/sharedprefsprovider.dart'; + +/// Reads the HF (MIFARE Classic / MIFARE Ultralight) dump from the currently +/// active slot's emulator memory and returns it as a [CardSave]. +Future readHfDumpFromSlot( + ChameleonCommunicator communicator, String name, TagType tagType) async { + CardData data = await communicator.mf1GetAntiCollData(); + + if (isMifareUltralight(tagType)) { + int pageCount = mfUltralightGetPagesCount(tagType); + List pages = []; + + for (int page = 0; page < pageCount; page++) { + pages.add(await communicator.mf0EmulatorReadPages(page, 1)); + } + + CardSaveExtra extraData = CardSaveExtra(); + + Uint8List version = await communicator.mf0EmulatorGetVersionData(); + if (version.isNotEmpty) { + extraData.ultralightVersion = version; + } + + Uint8List signature = await communicator.mf0EmulatorGetSignatureData(); + if (signature.isNotEmpty) { + extraData.ultralightSignature = signature; + } + + if (mfUltralightHasCounters(tagType)) { + List counters = []; + int counterCount = mfUltralightGetCounterCount(tagType); + + for (int i = 0; i < counterCount; i++) { + var counterData = await communicator.mf0EmulatorGetCounterData(i); + counters.add(counterData.$1); + } + + if (counters.isNotEmpty) { + extraData.ultralightCounters = counters; + } + } + + return CardSave( + uid: bytesToHexSpace(data.uid), + name: name, + sak: data.sak, + atqa: data.atqa, + ats: data.ats, + tag: tagType, + data: pages, + extraData: extraData, + ); + } else { + int blockCount = + mfClassicGetBlockCount(chameleonTagTypeGetMfClassicType(tagType)); + + Uint8List binData = Uint8List(blockCount * 16); + + int readCount = 16; + int binDataIndex = 0; + + for (int currentBlock = 0; + currentBlock < blockCount; + currentBlock += readCount) { + int blocksToRead = (currentBlock + readCount > blockCount) + ? blockCount - currentBlock + : readCount; + Uint8List result = + await communicator.mf1GetEmulatorBlock(currentBlock, blocksToRead); + + binData.setAll(binDataIndex, result); + binDataIndex += result.length; + } + + List blocks = []; + + for (int i = 0; i < binData.length; i += 16) { + blocks.add(Uint8List.fromList(binData.sublist(i, i + 16))); + } + + return CardSave( + uid: bytesToHexSpace(data.uid), + name: name, + sak: data.sak, + atqa: data.atqa, + ats: data.ats, + tag: tagType, + data: blocks, + ); + } +} + +/// Writes an edited HF (MIFARE Classic / MIFARE Ultralight) dump back into the +/// emulator memory of [slot] and persists it. [onProgress] is called with a +/// value from 0 to 100 while the dump is being uploaded. +Future writeHfDumpToSlot( + ChameleonCommunicator communicator, int slot, TagType tagType, + List dumpData, + {void Function(int progress)? onProgress}) async { + await communicator.activateSlot(slot); + + if (isMifareUltralight(tagType)) { + int pageCount = mfUltralightGetPagesCount(tagType); + + for (var page = 0; page < pageCount && dumpData.length > page; page++) { + await communicator.mf0EmulatorWritePages(page, dumpData[page]); + onProgress?.call((page / pageCount * 100).round()); + await asyncSleep(1); + } + } else { + int blockCount = + mfClassicGetBlockCount(chameleonTagTypeGetMfClassicType(tagType)); + + List blockChunk = []; + int lastSend = 0; + + for (var blockOffset = 0; blockOffset < blockCount; blockOffset++) { + if ((dumpData.length > blockOffset && dumpData[blockOffset].isEmpty) || + blockChunk.length >= 128) { + if (blockChunk.isNotEmpty) { + await communicator.setMf1BlockData( + lastSend, Uint8List.fromList(blockChunk)); + blockChunk = []; + lastSend = blockOffset; + } + } + + if (dumpData.length > blockOffset && dumpData[blockOffset].length == 16) { + blockChunk.addAll(dumpData[blockOffset]); + } + + onProgress?.call((blockOffset / blockCount * 100).round()); + await asyncSleep(1); + } + + if (blockChunk.isNotEmpty) { + await communicator.setMf1BlockData( + lastSend, Uint8List.fromList(blockChunk)); + } + } + + onProgress?.call(100); + await communicator.saveSlotData(); +} diff --git a/chameleonultragui/lib/helpers/write.dart b/chameleonultragui/lib/helpers/write.dart index 283618b3c..08ed7b6a2 100644 --- a/chameleonultragui/lib/helpers/write.dart +++ b/chameleonultragui/lib/helpers/write.dart @@ -58,7 +58,8 @@ abstract class AbstractWriteHelper { } if (isMifareUltralight(type)) { - return BaseMifareUltralightWriteHelper(appState.communicator!); + return BaseMifareUltralightWriteHelper(appState.communicator!, + tagType: type); } if (isEM410X(type) ||