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
79 changes: 69 additions & 10 deletions chameleonultragui/lib/gui/page/write_card.dart
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,54 @@ class WriteCardPageState extends State<WriteCardPage> {
});
}

Widget _buildForceWriteToggle() {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure we want this as a function? Wouldnt it make more sense as a stand alone stateful or stateless widget?

var localizations = AppLocalizations.of(context)!;
return Row(
children: [
Checkbox(
value: helper?.forceWrite ?? false,
onChanged: (val) {
setState(() {
if (helper != null) {
helper!.forceWrite = val ?? false;
}
});
},
),
GestureDetector(
onTap: () {
setState(() {
if (helper != null) {
helper!.forceWrite = !helper!.forceWrite;
}
});
},
child: Text(localizations.disable_acl_safety_check,
style: const TextStyle(fontSize: 13, color: Colors.red)),
),
],
);
}

Widget _buildAclWarnings() {
if (helper!.autoCorrectedBlocks.isEmpty &&
helper!.dangerousBlocks.isEmpty) {
return const SizedBox.shrink();
}
var localizations = AppLocalizations.of(context)!;
return Column(children: [
const SizedBox(height: 8),
if (helper!.autoCorrectedBlocks.isNotEmpty)
Text(localizations.acl_invalid_corrected(
helper!.autoCorrectedBlocks.join(', ')),
style: const TextStyle(color: Colors.orange, fontSize: 12)),
if (helper!.dangerousBlocks.isNotEmpty)
Text(localizations.acl_dangerous_blocked(
helper!.dangerousBlocks.join(', ')),
style: const TextStyle(color: Colors.red, fontSize: 12)),
]);
}

Future<void> writeCard() async {
var appState = Provider.of<ChameleonGUIState>(context, listen: false);
var scaffoldMessenger = ScaffoldMessenger.of(context);
Expand Down Expand Up @@ -224,6 +272,9 @@ class WriteCardPageState extends State<WriteCardPage> {
if (step == 1) {
await helper?.reset();
}

helper?.autoCorrectedBlocks.clear();
helper?.dangerousBlocks.clear();
}

void onStepReset() async {
Expand Down Expand Up @@ -375,7 +426,9 @@ class WriteCardPageState extends State<WriteCardPage> {
},
child:
Text(localizations.auto_detect_magic_card),
)
),
const SizedBox(width: 16),
_buildForceWriteToggle(),
])
: Text(localizations.writing_is_not_yet_supported),
),
Expand All @@ -392,15 +445,21 @@ class WriteCardPageState extends State<WriteCardPage> {
helper!.getFailedBlocks().isNotEmpty)
? Text(
"${localizations.otp_magic_warning(localizations.write_data_to_magic_card)} ${localizations.some_blocks_failed_to_write}: ${helper!.getFailedBlocks().join(", ")}")
: Column(children: [
Text(localizations.otp_magic_warning(
localizations.write_data_to_magic_card)),
const SizedBox(height: 8),
Text(localizations.keep_stable_warning,
style: const TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold))
])
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(localizations.otp_magic_warning(
localizations.write_data_to_magic_card)),
const SizedBox(height: 8),
Text(localizations.keep_stable_warning,
style: const TextStyle(
color: Colors.orange,
fontWeight: FontWeight.bold)),
_buildAclWarnings(),
const SizedBox(height: 12),
_buildForceWriteToggle(),
],
)
: (helper != null && helper!.writeWidgetSupported())
? helper!.getWriteWidget(context, setState)
: Text(localizations.error)
Expand Down
58 changes: 58 additions & 0 deletions chameleonultragui/lib/helpers/mifare_classic/dump_analyzer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,62 @@ class MifareClassicDumpAnalyzer {
static bool isValidBlock(String text) {
return RegExp(r'^[0-9A-Fa-f-]{32}$').hasMatch(text.trim());
}

/// Validate MIFARE Classic access condition bytes (positions 6-8 of sector
/// trailer). Returns true if the complement bits match the original bits.
/// Invalid ACL (e.g. all zeros) will brick the sector permanently.
static bool isValidAcl(Uint8List sectorTrailer) {
if (sectorTrailer.length < 9) return false;
final b6 = sectorTrailer[6]; // bits 7-4: ~C2, bits 3-0: ~C1
final b7 = sectorTrailer[7]; // bits 7-4: C1, bits 3-0: ~C3
final b8 = sectorTrailer[8]; // bits 7-4: C3, bits 3-0: C2

// ~C1 must complement C1
if ((~b6 & 0x0F) != ((b7 >> 4) & 0x0F)) return false;
// ~C2 must complement C2
if ((~(b6 >> 4) & 0x0F) != (b8 & 0x0F)) return false;
// ~C3 must complement C3
if ((~b7 & 0x0F) != ((b8 >> 4) & 0x0F)) return false;

return true;
}

/// Check whether the sector trailer contains dangerous access conditions
/// that would permanently lock blocks. Returns a list of locked block
/// numbers (0-3 within the sector), or empty list if safe.
///
/// Matches Proxmark3's mfReadOnlyAccessConditions logic.
static List<int> getDangerousAclBlocks(Uint8List sectorTrailer) {
if (sectorTrailer.length < 9) return [];
final b7 = sectorTrailer[7]; // C1[7:4] ~C3[3:0]
final b8 = sectorTrailer[8]; // C3[7:4] C2[3:0]

// Only consider ACL valid for this check
if (!isValidAcl(sectorTrailer)) return [];

final c1 = (b7 >> 4) & 0x0F; // C1_3 C1_2 C1_1 C1_0
final c2 = b8 & 0x0F; // C2_3 C2_2 C2_1 C2_0
final c3 = (b8 >> 4) & 0x0F; // C3_3 C3_2 C3_1 C3_0

final dangerous = <int>[];
for (int blockn = 0; blockn < 4; blockn++) {
final c1b = (c1 >> blockn) & 1;
final c2b = (c2 >> blockn) & 1;
final c3b = (c3 >> blockn) & 1;
final cond = (c1b << 2) | (c2b << 1) | c3b;

if (blockn == 3) {
// Sector trailer: conditions 2, 6, 7 are dangerous (lock keys/ACL)
if (cond == 2 || cond == 6 || cond == 7) {
dangerous.add(blockn);
}
} else {
// Data blocks: conditions 2, 5 are read-only / permanently locked
if (cond == 2 || cond == 5) {
dangerous.add(blockn);
}
}
}
return dangerous;
}
}
56 changes: 56 additions & 0 deletions chameleonultragui/lib/helpers/mifare_classic/write/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:chameleonultragui/gui/component/mifare/classic.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_classic/dump_analyzer.dart';
import 'package:chameleonultragui/helpers/mifare_classic/general.dart';
import 'package:chameleonultragui/helpers/mifare_classic/recovery.dart';
import 'package:chameleonultragui/helpers/mifare_classic/write/gen1.dart';
Expand Down Expand Up @@ -93,6 +94,8 @@ class BaseMifareClassicWriteHelper extends AbstractWriteHelper {
Future<bool> writeData(
CardSave card, Function(int writeProgress) update) async {
List<Uint8List> data = card.data;
autoCorrectedBlocks.clear();
dangerousBlocks.clear();

if (await communicator.scan14443aTag() == null) {
return false;
Expand All @@ -105,6 +108,21 @@ class BaseMifareClassicWriteHelper extends AbstractWriteHelper {
data[0] = createBlock0FromSave(card);
}

// First pass: sanitize all sector trailers, collect all ACL warnings.
// Don't write anything if any dangerous ACL is blocked.
for (var sector = 0;
sector < mfClassicGetSectorCount(type, isEV1: isEV1);
sector++) {
final blockToWrite = mfClassicGetSectorTrailerBlockBySector(sector);
if (data.length > blockToWrite && data[blockToWrite].isNotEmpty) {
sanitizeSectorTrailerAcl(data[blockToWrite], sector: sector);
}
}

if (dangerousBlocks.isNotEmpty) {
return false;
}

for (var sector = 0;
sector < mfClassicGetSectorCount(type, isEV1: isEV1);
sector++) {
Expand All @@ -125,6 +143,42 @@ class BaseMifareClassicWriteHelper extends AbstractWriteHelper {
return true;
}

/// Check sector trailer ACL safety before writing.
/// Returns true if writing should proceed, false if blocked.
///
/// - Invalid ACL (complement mismatch): ALWAYS auto-corrected to FF 07 80 69.
/// - Dangerous ACL (would permanently lock blocks): blocked unless
/// [forceWrite] is true.
bool sanitizeSectorTrailerAcl(Uint8List sectorTrailer, {int sector = -1}) {
if (sectorTrailer.length < 10) return false;

final trailerBlock = mfClassicGetSectorTrailerBlockBySector(sector);
final firstBlock = mfClassicGetFirstBlockCountBySector(sector);

// Check 1: illegal ACL → always auto-correct (even with forceWrite)
if (!MifareClassicDumpAnalyzer.isValidAcl(sectorTrailer)) {
autoCorrectedBlocks.add(trailerBlock);
sectorTrailer[6] = 0xFF;
sectorTrailer[7] = 0x07;
sectorTrailer[8] = 0x80;
sectorTrailer[9] = 0x69;
return true;
}

// Check 2: valid but dangerous — blocked unless forceWrite
if (!forceWrite) {
final dangerous = MifareClassicDumpAnalyzer.getDangerousAclBlocks(sectorTrailer);
if (dangerous.isNotEmpty) {
for (final blockn in dangerous) {
dangerousBlocks.add(firstBlock + blockn);
}
return false;
}
}

return true;
}

Uint8List createBlock0FromSave(CardSave card) {
List<int> block = [];
Uint8List uid = hexToBytes(card.uid);
Expand Down Expand Up @@ -177,6 +231,8 @@ class BaseMifareClassicWriteHelper extends AbstractWriteHelper {
Future<void> reset() async {
hfInfo = null;
mfcInfo = null;
autoCorrectedBlocks.clear();
dangerousBlocks.clear();
}

@override
Expand Down
5 changes: 5 additions & 0 deletions chameleonultragui/lib/helpers/mifare_classic/write/gen2.dart
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,8 @@ class MifareClassicGen2WriteHelper extends BaseMifareClassicWriteHelper {
List<Uint8List> data = card.data;
List<bool> cleanSectors = List.generate(40, (index) => false);
failedBlocks = [];
autoCorrectedBlocks.clear();
dangerousBlocks.clear();

try {
await communicator.scan14443aTag();
Expand All @@ -129,6 +131,9 @@ class MifareClassicGen2WriteHelper extends BaseMifareClassicWriteHelper {
for (var sector = 0; sector < mfClassicGetSectorCount(type); sector++) {
var block = mfClassicGetSectorTrailerBlockBySector(sector);
if (data.length > block && data[block].isNotEmpty) {
if (!sanitizeSectorTrailerAcl(data[block], sector: sector)) {
continue; // dangerous ACL blocked
}
cleanSectors[sector] = await writeBlockModifier(
card, block, data[block],
tryBothKeys: true);
Expand Down
11 changes: 11 additions & 0 deletions chameleonultragui/lib/helpers/write.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ abstract class AbstractWriteHelper {
bool readSupported = false; // can read data without authorization
bool writeSupported = false; // can write data without authorization

/// When true, dangerous (read-only/locking) ACL is written as-is.
/// Invalid ACL (complement mismatch) is always auto-corrected regardless
/// of this setting. Default false (safety ON).
bool forceWrite = false;

/// Blocks with invalid ACL auto-corrected to FF 07 80 69.
final List<int> autoCorrectedBlocks = [];

/// Blocks with dangerous ACL that were blocked from writing.
final List<int> dangerousBlocks = [];

String get name => "Abstract"; // name in dropdown
static String get staticName => "Abstract"; // for comparing

Expand Down
5 changes: 4 additions & 1 deletion chameleonultragui/lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -479,5 +479,8 @@
"lf_sniff_hex_glyph_carrier": "carrier",
"lf_sniff_hex_glyph_high": "high",
"lf_sniff_hex_glyph_clipped": "clipped",
"lf_sniff_level_legend": "_ gap . ringing - low + mid o carrier O high # clipped"
"lf_sniff_level_legend": "_ gap . ringing - low + mid o carrier O high # clipped",
"disable_acl_safety_check": "Disable ACL safety check",
"acl_invalid_corrected": "Invalid ACL auto-corrected at blocks: {blocks}",
"acl_dangerous_blocked": "Dangerous ACL blocked at blocks: {blocks}. Enable \"Disable ACL safety check\" to override."
}
5 changes: 4 additions & 1 deletion chameleonultragui/lib/l10n/app_zh.arb
Original file line number Diff line number Diff line change
Expand Up @@ -400,5 +400,8 @@
"android_ble_permissions_missing": "\u7f3a\u5c11\u84dd\u7259\u6216\u4f4d\u7f6e\u6743\u9650\u3002\u8981\u901a\u8fc7\u84dd\u7259\u8fde\u63a5\uff0c\u8bf7\u5728\u8bbe\u5907\u7684\u8bbe\u7f6e\u5e94\u7528\u7a0b\u5e8f\u4e2d\u6388\u4e88\u6743\u9650",
"skip_recovery": "\u8df3\u8fc7\u6062\u590d",
"resume_recovery": "\u7ee7\u7eed\u6062\u590d",
"language_name": "\u7b80\u4f53\u4e2d\u6587"
"language_name": "\u7b80\u4f53\u4e2d\u6587",
"disable_acl_safety_check": "\u7981\u7528 ACL \u5b89\u5168\u68c0\u67e5",
"acl_invalid_corrected": "\u975e\u6cd5 ACL \u5df2\u81ea\u52a8\u4fee\u6b63\uff0c\u5757\uff1a{blocks}",
"acl_dangerous_blocked": "\u5371\u9669 ACL \u5df2\u7981\u6b62\uff0c\u5757\uff1a{blocks}\u3002\u52fe\u9009\u3010\u7981\u7528 ACL \u5b89\u5168\u68c0\u67e5\u3011\u53ef\u5f3a\u5236\u5199\u5165\u3002"
}
Loading