DEV Community

Aethel-Systems
Aethel-Systems

Posted on

I Kept Bricking NFC Cards — So I Built a 1KB Crash-Proof Storage Engine

The "Tear-off" Nightmare

If you’ve ever worked with NFC development, you know the "Tear-off" effect.

You are in the middle of writing a block of data to a MIFARE Classic card. The user’s hand shakes, the phone moves 1cm away from the tag, and boom—the RF field collapses. You just suffered a partial write. The data is corrupted, the checksums fail, and in the worst-case scenario, your application-level logic "bricks" the card because it's now in an inconsistent state.

In my project AECardTools, I decided that "good enough" wasn't enough. I wanted an NFC storage system that was physically impossible to brick, even if you pull the card away mid-write.

Here is how I built a Log-structured Copy-on-Write (LCOW) engine that fits inside a tiny 1KB NFC chip.


The Challenge: 1024 Bytes of Chaos

A MIFARE Classic 1K card is not a "hard drive." It's a collection of 16 sectors, each protected by keys. Normally, developers overwrite data in place. If the power cuts at byte 8 of 16, you are toast.

To solve this, I implemented three core concepts:

  1. Never Overwrite: We never modify the "current" valid data. We always write to a new location.
  2. Atomic Commits: Data is only considered "real" once a single, tiny pointer (the Anchor) is updated at the very end.
  3. Maximizing Payload (The 900-Byte Hack): Standard MIFARE 1K usually only gives you 752 bytes of user data because the "Sector Trailers" (which store Keys A and B) take up space.

The Trick: In AECardTools, I treat the card as a raw encrypted canvas. I store the Sector Keys locally in the app's encrypted database. This allows me to reclaim the Key A/B areas for data storage, pushing the usable capacity to roughly 900 bytes.


The Architecture: How LCOW Works

The engine (written in Python via Chaquopy for the heavy lifting, and Kotlin for the NFC I/O) treats the card as a series of versioned logs.

1. The Ping-Pong Anchors

I reserved the very last sector (Sector 15) as the "Command Center." It contains two "Anchors."

  • Anchor A and Anchor B act like a toggle switch.
  • Each anchor contains a Transaction Sequence Number (TSN) and a pointer to the current root block.

2. The Write Flow

When you save a new file to the card:

  1. Find Free Space: The engine looks for sectors not occupied by the current version.
  2. Write New Data: It writes the new encrypted payload to these "shadow" sectors. If the user pulls the card away now, the old data is still sitting safely in its original sectors.
  3. The Atomic Flip: Only after the data is 100% verified does the engine write a new TSN+1 to the other Anchor.

3. The Recovery

When the app scans a card, it looks at both Anchors.

  • If Anchor A says TSN: 10 and Anchor B says TSN: 11, the engine instantly knows that Version 11 is the truth.
  • If the write was interrupted, Version 11 would have a failed CRC or an older TSN, and the engine would automatically "roll back" to Version 10. Zero data loss. Zero bricking.

4. Architecture diagram

To bridge the gap between high-level Python logic and raw Android NFC hardware, I implemented a Neuromorphic FFI Bridge. Here is the high-level architecture:

┌─────────────────────────────────────────────────────────────────┐
│              AECardTools: Sovereign Architecture                │
├─────────────────────────────────────────────────────────────────┤
│ [UI Layer] (Kotlin / Jetpack Compose)                           │
│  - HexCanvas & Registry Editor UI                               │
│  - Security Disclaimer & Transaction Monitoring                 │
└───────────────┬─────────────────────────────────────────────────┘
                │ Reactive StateFlow
┌───────────────▼─────────────────────────────────────────────────┐
│ [ViewModel / Session Manager]                                   │
│  - Global NFC Session tracking                                  │
│  - Hardware I/O Orchestration                                   │
└───────────────┬─────────────────────────────────────────────────┘
                │ JNI / Chaquopy FFI Bridge
┌───────────────▼─────────────────────────────────────────────────┐
│ [Python Core Engine] (The "Brain")                              │
│  ┌────────────────────┐    ┌─────────────────────────────────┐  │
│  │    LCOW Engine     │    │      Cryptography Module        │  │
│  │ - Virtual Address  │    │ - Argon2id Key Derivation       │  │
│  │ - Transaction Logs │    │ - XChaCha20-Poly1305 (AEAD)     │  │
│  │ - GC Controller    │    │ - Merkle Tree Integrity Check   │  │
│  └──────────┬─────────┘    └─────────────────────────────────┘  │
└─────────────┼───────────────────────────────────────────────────┘
              │ Callbacks
┌─────────────▼───────────────────────────────────────────────────┐
│ [NFC Hardware Layer] (Kotlin / android.nfc.tech)                │
│  - Universal Protocol Manager (IsoDep / NfcA / NfcB)            │
│  - Sensitive Instruction Interceptor (Brick Protection)         │
└──────────────────────────────┬──────────────────────────────────┘
                               │ RF Field (13.56MHz)
                    ┌──────────▼──────────┐
                    │  MIFARE Classic 1K  │
                    └─────────────────────┘
Enter fullscreen mode Exit fullscreen mode

The real magic happens in the memory remapping. I treat the 16 sectors not as rigid silos, but as a virtualized block pool managed by the LCOW engine:

PHYSICAL STORAGE (1024B)             LOGICAL VIEW (AEFS v5.5)
┌──────────────────────────┐         ┌──────────────────────────┐
│ Sector 0: Genesis Block  │────────▶│ 0x000 - 0x01F: Metadata  │
├──────────────────────────┤         ├──────────────────────────┤
│ Sectors 1-14:            │         │ 0x020 - 0x2EF:           │
│                          │         │                          │
│ [ LOG-STRUCTURED POOL ]  │────────▶│ [ SEAMLESS PAYLOAD ]     │
│ (42 Data Blocks)         │         │                          │
│                          │         │ (Encrypted Canvas)       │
├──────────────────────────┤         └──────────────────────────┘
│ Sector 15: Superblock    │
│ [Block 0]: Anchor A (Ping)◀───┐      ATOMIC FLIP LOGIC:
│ [Block 1]: Anchor B (Pong)◀───┴──[ Active pointer toggles ]
│ [Block 2]: GC / Bitmap   │       [ only after successful  ]
│ [Block 3]: Keys & ACL    │       [ verify-after-write     ]
└──────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Hardening the Security: Sovereign Keys

Since I'm using the Key A/B sectors for data to hit that 900-byte goal, the security model changes.

Most NFC apps rely on the card's hardware to check keys. But MIFARE Classic's "Crypto1" algorithm is famously broken. Instead, I implement Sovereign Encryption:

  • The card is just a "dumb" encrypted block device.
  • The encryption (XChaCha20-Poly1305) and key derivation (Argon2id) happen entirely inside the Android app.
  • Even if someone cracks the RFID hardware layer, they just see a 900-byte blob of high-entropy noise.

The Result

By combining a Copy-on-Write strategy with a Local Key Management system, I turned a cheap $0.50 RFID tag into a robust, atomic, and encrypted mini-vault.

It’s a reminder that even when working with "ancient" hardware from the 90s, modern software patterns like LCOW can solve physical-layer reliability problems.

If you're building anything with NFC, you might find this useful 👇

👉 GitHub:
AECardTools

Top comments (0)