diff --git a/chameleonultragui/lib/bridge/chameleon.dart b/chameleonultragui/lib/bridge/chameleon.dart index 2d7c67ec6..a26366892 100644 --- a/chameleonultragui/lib/bridge/chameleon.dart +++ b/chameleonultragui/lib/bridge/chameleon.dart @@ -592,7 +592,15 @@ class ChameleonCommunicator { } Future setEM410XEmulatorID(Uint8List uid) async { - await sendCmd(ChameleonCommand.setEM410XemulatorID, data: uid); + // Firmware accepts only exactly 5 (EM410X) or 13 (Electra) bytes and + // rejects type-prefixed payloads that older saves may still contain. + final Uint8List normalized = normalizeEm410xUid(uid); + final resp = await sendCmd(ChameleonCommand.setEM410XemulatorID, + data: normalized); + if (resp != null && resp.status != 0x68) { + throw Exception( + 'Failed to set EM410X emulator ID (status=0x${resp.status.toRadixString(16)}, len=${normalized.length})'); + } } Future setHIDProxEmulatorID(Uint8List uid) async { diff --git a/chameleonultragui/lib/gui/menu/dialogs/card/create.dart b/chameleonultragui/lib/gui/menu/dialogs/card/create.dart index a42fe2e9c..474416996 100644 --- a/chameleonultragui/lib/gui/menu/dialogs/card/create.dart +++ b/chameleonultragui/lib/gui/menu/dialogs/card/create.dart @@ -392,6 +392,10 @@ class CardCreateMenuState extends State { ); finalUid = hidCard.toString(); + } else if (isEM410X(selectedType)) { + finalUid = bytesToHexSpace(normalizeEm410xUid( + hexToBytes(uidController.text), + type: selectedType)); } else { finalUid = bytesToHexSpace(hexToBytes(uidController.text)); } diff --git a/chameleonultragui/lib/gui/menu/dialogs/card/edit.dart b/chameleonultragui/lib/gui/menu/dialogs/card/edit.dart index 155588c9f..bc3df214c 100644 --- a/chameleonultragui/lib/gui/menu/dialogs/card/edit.dart +++ b/chameleonultragui/lib/gui/menu/dialogs/card/edit.dart @@ -499,6 +499,11 @@ class CardEditMenuState extends State { } catch (e) { finalUid = bytesToHexSpace(hexToBytes(uidController.text)); } + } else if (isEM410X(selectedType)) { + // Persist clean 5/13-byte UID only (strip legacy type prefix). + finalUid = bytesToHexSpace(normalizeEm410xUid( + hexToBytes(uidController.text), + type: selectedType)); } else { finalUid = bytesToHexSpace(hexToBytes(uidController.text)); } diff --git a/chameleonultragui/lib/gui/menu/dialogs/slot/edit.dart b/chameleonultragui/lib/gui/menu/dialogs/slot/edit.dart index c945becfe..6ca19f367 100644 --- a/chameleonultragui/lib/gui/menu/dialogs/slot/edit.dart +++ b/chameleonultragui/lib/gui/menu/dialogs/slot/edit.dart @@ -187,23 +187,30 @@ class SlotEditMenuState extends State { var appState = Provider.of(context, listen: false); await appState.communicator!.activateSlot(widget.slot); - if (widget.slotType != selectedType) { - await appState.communicator!.setSlotType(widget.slot, selectedType!); + + // EM410x 16/32/64 are reader clock modes only; firmware emulates base type. + TagType effectiveType = isEM410X(selectedType!) + ? em410xSlotTagType(selectedType!) + : selectedType!; + + if (widget.slotType != effectiveType) { + await appState.communicator!.setSlotType(widget.slot, effectiveType); bool oldIsClassic = isMifareClassic(widget.slotType); - bool newIsClassic = isMifareClassic(selectedType!); + bool newIsClassic = isMifareClassic(effectiveType); bool oldIsUltralight = isMifareUltralight(widget.slotType); - bool newIsUltralight = isMifareUltralight(selectedType!); + bool newIsUltralight = isMifareUltralight(effectiveType); if (!((oldIsClassic && newIsClassic) || (oldIsUltralight && newIsUltralight))) { await appState.communicator! - .setDefaultDataToSlot(widget.slot, selectedType!); + .setDefaultDataToSlot(widget.slot, effectiveType); } } if (isEM410X(selectedType!)) { - await appState.communicator! - .setEM410XEmulatorID(hexToBytes(uidController.text)); + await appState.communicator!.setEM410XEmulatorID(normalizeEm410xUid( + hexToBytes(uidController.text), + type: selectedType)); } else if (selectedType! == TagType.hidProx) { try { int hidType = int.parse(hidTypeController.text); diff --git a/chameleonultragui/lib/gui/page/read_card.dart b/chameleonultragui/lib/gui/page/read_card.dart index d23796ab4..9c254bbd4 100644 --- a/chameleonultragui/lib/gui/page/read_card.dart +++ b/chameleonultragui/lib/gui/page/read_card.dart @@ -279,8 +279,12 @@ class ReadCardPageState extends State { var appState = Provider.of(context, listen: false); var tags = appState.sharedPreferencesProvider.getCards(); - tags.add(CardSave( - uid: lfInfo.card.toString(), name: dumpName, tag: lfInfo.card!.type)); + final card = lfInfo.card!; + // Always store a clean UID (no type-prefix) so load-to-slot works. + final String uid = isEM410X(card.type) + ? bytesToHexSpace(normalizeEm410xUid(card.uid, type: card.type)) + : card.toString(); + tags.add(CardSave(uid: uid, name: dumpName, tag: card.type)); appState.sharedPreferencesProvider.setCards(tags); } diff --git a/chameleonultragui/lib/gui/page/slot_manager.dart b/chameleonultragui/lib/gui/page/slot_manager.dart index 3d745d86f..e0b40f30a 100644 --- a/chameleonultragui/lib/gui/page/slot_manager.dart +++ b/chameleonultragui/lib/gui/page/slot_manager.dart @@ -155,13 +155,14 @@ class SlotManagerPageState extends State { await appState.communicator! .enableSlot(gridPosition, TagFrequency.lf, true); await appState.communicator!.activateSlot(gridPosition); - TagType slotTagType = card.tag == TagType.em410XElectra - ? TagType.em410XElectra - : TagType.em410X; + // 16/32/64 are reader clock modes only; firmware emulates base EM410X. + TagType slotTagType = em410xSlotTagType(card.tag); await appState.communicator!.setSlotType(gridPosition, slotTagType); await appState.communicator! .setDefaultDataToSlot(gridPosition, slotTagType); - await appState.communicator!.setEM410XEmulatorID(hexToBytes(card.uid)); + // Normalize UID: strip type prefix from legacy saves, enforce 5/13 bytes. + await appState.communicator!.setEM410XEmulatorID( + normalizeEm410xUid(hexToBytes(card.uid), type: card.tag)); await appState.communicator!.setSlotTagName( gridPosition, (card.name.isEmpty) ? localizations.no_name : card.name, diff --git a/chameleonultragui/lib/helpers/card_save_converters.dart b/chameleonultragui/lib/helpers/card_save_converters.dart index 8378adc91..c954487ee 100644 --- a/chameleonultragui/lib/helpers/card_save_converters.dart +++ b/chameleonultragui/lib/helpers/card_save_converters.dart @@ -158,7 +158,9 @@ CardSave flipperRfidToCardSave(String data) { switch (type) { case 'EM4100': - tag = TagType.em410X64; + // Flipper "EM4100" is standard 64-clock EM410x; store as base EM410X + // so load-to-slot maps cleanly to firmware emulation type. + tag = TagType.em410X; break; case 'EM4100/32': tag = TagType.em410X32; @@ -181,5 +183,9 @@ CardSave flipperRfidToCardSave(String data) { tag = TagType.unknown; } + if (isEM410X(tag)) { + uid = bytesToHexSpace(normalizeEm410xUid(hexToBytes(uid), type: tag)); + } + return CardSave(id: id, uid: uid, name: uid, tag: tag, color: color); } diff --git a/chameleonultragui/lib/helpers/general.dart b/chameleonultragui/lib/helpers/general.dart index c8c66cbe3..e7fbee3ff 100644 --- a/chameleonultragui/lib/helpers/general.dart +++ b/chameleonultragui/lib/helpers/general.dart @@ -32,15 +32,71 @@ String bytesToHexSpace(Uint8List bytes) { } Uint8List hexToBytes(String hex) { - hex = hex.replaceAll(" ", ""); + // Strip spaces, colons, and any other non-hex separators users may enter. + hex = hex.replaceAll(RegExp(r'[^0-9A-Fa-f]'), ''); List bytes = []; - for (int i = 0; i < hex.length; i += 2) { + for (int i = 0; i + 1 < hex.length; i += 2) { int byte = int.parse(hex.substring(i, i + 2), radix: 16); bytes.add(byte); } return Uint8List.fromList(bytes); } +/// Normalize EM410x UID bytes for firmware set/get. +/// +/// New firmware returns `type(u16 BE) + uid` on scan/get. Older GUI builds +/// sometimes saved that full payload as the card UID, so load-to-slot would +/// send 7 bytes and firmware rejects with PAR_ERR (expects exactly 5 or 13). +/// Manual 5-byte entry worked; load from saved card did not. +Uint8List normalizeEm410xUid(Uint8List data, {TagType? type}) { + final bool electra = type == TagType.em410XElectra; + final int expected = electra ? 13 : 5; + + if (data.isEmpty) { + return data; + } + + // Already a clean firmware-ready UID. + if (data.length == 5 || data.length == 13) { + return data; + } + + // Prefer type-prefixed payload: [type_hi, type_lo, ...uid] + if (data.length >= 2) { + final TagType prefixType = + numberToChameleonTag(bytesToU16(data.sublist(0, 2))); + if (isEM410X(prefixType)) { + final int uidLength = uidSizeForLfTag(prefixType); + if (uidLength > 0 && data.length >= 2 + uidLength) { + return data.sublist(2, 2 + uidLength); + } + } + + // Unknown/legacy prefix with classic EM410x size (2 + 5) or Electra (2 + 13) + if (data.length == 7) { + return data.sublist(2, 7); + } + if (data.length == 15) { + return data.sublist(2, 15); + } + } + + if (data.length > expected) { + return data.sublist(0, expected); + } + + return data; +} + +/// EM410x variants 16/32/64 are reader clock modes only; firmware emulates +/// only [TagType.em410X] and [TagType.em410XElectra]. +TagType em410xSlotTagType(TagType type) { + if (type == TagType.em410XElectra) { + return TagType.em410XElectra; + } + return TagType.em410X; +} + int bytesToU8(Uint8List byteArray) { return byteArray.buffer.asByteData().getUint8(0); } diff --git a/chameleonultragui/lib/helpers/t55xx/write/base.dart b/chameleonultragui/lib/helpers/t55xx/write/base.dart index cc8801931..0d6855a3a 100644 --- a/chameleonultragui/lib/helpers/t55xx/write/base.dart +++ b/chameleonultragui/lib/helpers/t55xx/write/base.dart @@ -147,7 +147,8 @@ class BaseT55XXCardHelper extends AbstractWriteHelper { Future writeData( CardSave card, Function(int writeProgress) update) async { if (isEM410X(card.tag)) { - await communicator.writeEM410XtoT55XX(hexToBytes(card.uid), + await communicator.writeEM410XtoT55XX( + normalizeEm410xUid(hexToBytes(card.uid), type: card.tag), hexToBytes(newKey), [hexToBytes(currentKey), Uint8List(4)]); await Future.delayed(const Duration(milliseconds: 500)); var newCard = await communicator.readEM410X(); diff --git a/chameleonultragui/test/em410x_uid_test.dart b/chameleonultragui/test/em410x_uid_test.dart new file mode 100644 index 000000000..793cc22a4 --- /dev/null +++ b/chameleonultragui/test/em410x_uid_test.dart @@ -0,0 +1,60 @@ +import 'dart:typed_data'; + +import 'package:chameleonultragui/helpers/definitions.dart'; +import 'package:chameleonultragui/helpers/general.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('normalizeEm410xUid', () { + test('passes through clean 5-byte UID', () { + final uid = Uint8List.fromList([0x01, 0x23, 0x45, 0x67, 0x89]); + expect(normalizeEm410xUid(uid), uid); + }); + + test('passes through clean 13-byte Electra UID', () { + final uid = Uint8List.fromList(List.generate(13, (i) => i + 1)); + expect(normalizeEm410xUid(uid, type: TagType.em410XElectra), uid); + }); + + test('strips type prefix from 7-byte scan payload (legacy saved cards)', () { + // type = EM410X (100 = 0x0064) + 5-byte UID + final raw = Uint8List.fromList([0x00, 0x64, 0xAB, 0xCD, 0xEF, 0x01, 0x23]); + expect( + normalizeEm410xUid(raw), + Uint8List.fromList([0xAB, 0xCD, 0xEF, 0x01, 0x23]), + ); + }); + + test('strips EM410X_64 type prefix (0x0067)', () { + final raw = Uint8List.fromList([0x00, 0x67, 0x11, 0x22, 0x33, 0x44, 0x55]); + expect( + normalizeEm410xUid(raw, type: TagType.em410X64), + Uint8List.fromList([0x11, 0x22, 0x33, 0x44, 0x55]), + ); + }); + + test('hexToBytes strips colons and spaces', () { + expect( + hexToBytes('AB:CD EF-01 23'), + Uint8List.fromList([0xAB, 0xCD, 0xEF, 0x01, 0x23]), + ); + }); + + test('em410xSlotTagType maps variants to base EM410X', () { + expect(em410xSlotTagType(TagType.em410X), TagType.em410X); + expect(em410xSlotTagType(TagType.em410X16), TagType.em410X); + expect(em410xSlotTagType(TagType.em410X32), TagType.em410X); + expect(em410xSlotTagType(TagType.em410X64), TagType.em410X); + expect(em410xSlotTagType(TagType.em410XElectra), TagType.em410XElectra); + }); + + test('round-trip: legacy save string load yields 5 bytes', () { + // Simulates old app saving full scan response as UID hex + final legacyUidHex = bytesToHexSpace( + Uint8List.fromList([0x00, 0x64, 0xDE, 0xAD, 0xBE, 0xEF, 0x01])); + final loaded = normalizeEm410xUid(hexToBytes(legacyUidHex)); + expect(loaded.length, 5); + expect(loaded, Uint8List.fromList([0xDE, 0xAD, 0xBE, 0xEF, 0x01])); + }); + }); +}