Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion chameleonultragui/lib/bridge/chameleon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,15 @@ class ChameleonCommunicator {
}

Future<void> 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<void> setHIDProxEmulatorID(Uint8List uid) async {
Expand Down
4 changes: 4 additions & 0 deletions chameleonultragui/lib/gui/menu/dialogs/card/create.dart
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@ class CardCreateMenuState extends State<CardCreateMenu> {
);

finalUid = hidCard.toString();
} else if (isEM410X(selectedType)) {
finalUid = bytesToHexSpace(normalizeEm410xUid(
hexToBytes(uidController.text),
type: selectedType));
} else {
finalUid = bytesToHexSpace(hexToBytes(uidController.text));
}
Expand Down
5 changes: 5 additions & 0 deletions chameleonultragui/lib/gui/menu/dialogs/card/edit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,11 @@ class CardEditMenuState extends State<CardEditMenu> {
} 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));
}
Expand Down
21 changes: 14 additions & 7 deletions chameleonultragui/lib/gui/menu/dialogs/slot/edit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -187,23 +187,30 @@ class SlotEditMenuState extends State<SlotEditMenu> {
var appState = Provider.of<ChameleonGUIState>(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);
Expand Down
8 changes: 6 additions & 2 deletions chameleonultragui/lib/gui/page/read_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,12 @@ class ReadCardPageState extends State<ReadCardPage> {
var appState = Provider.of<ChameleonGUIState>(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);
}

Expand Down
9 changes: 5 additions & 4 deletions chameleonultragui/lib/gui/page/slot_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,14 @@ class SlotManagerPageState extends State<SlotManagerPage> {
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,
Expand Down
8 changes: 7 additions & 1 deletion chameleonultragui/lib/helpers/card_save_converters.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
60 changes: 58 additions & 2 deletions chameleonultragui/lib/helpers/general.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> 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);
}
Expand Down
3 changes: 2 additions & 1 deletion chameleonultragui/lib/helpers/t55xx/write/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ class BaseT55XXCardHelper extends AbstractWriteHelper {
Future<bool> 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();
Expand Down
60 changes: 60 additions & 0 deletions chameleonultragui/test/em410x_uid_test.dart
Original file line number Diff line number Diff line change
@@ -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<int>.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]));
});
});
}
Loading