Skip to main content
ZeroKeyUSB encrypts each credential block using AES-128 in CBC mode. Single-block ECB is done on the ATECC608A using a key that never leaves the chip; the SAMD21 MCU wraps the chip calls in CBC chaining and handles I/O to the EEPROM.

Key material

The AES master key is a 16-byte random value generated by the ATECC608A TRNG at first boot, written to slot 8 of the chip, and then locked in by the data-zone lock. Because IsSecret=1 is enforced before the data zone is locked, even an attacker with physical I²C access cannot read the AES key back from the chip — the Read command refuses to return it.

Why the chip and not the MCU?

The earlier firmware ran software AES on the SAMD21 with the key stored in EEPROM, because the MAHDA-T variant of the ATECC608A ships with the hardware AES command disabled. Enabling it requires:
  1. Writing the AES_Enable bit (byte 13, bit 0) of the Config Zone.
  2. Configuring slot 8 with KeyType=6 (AES).
  3. Locking the Config Zone so those settings take effect.
The firmware now does all three the first time it boots. The trade-off: AES blocks now cross the I²C bus, which is slower (~10 ms per block vs ~0.1 ms in software). For a credential read that decrypts 3×32 bytes that’s ≈60 ms — imperceptible to the user.

CBC chaining implementation

The cbcEncrypt32 / cbcDecrypt32 functions in zerokey-security.cpp process each 32-byte credential field as two 16-byte blocks chained against the device IV:

Encryption

Decryption

The MCU only sees plaintext blocks (input to encrypt, output of decrypt) and ciphertext blocks (output of encrypt, input to decrypt). It never sees the AES key.

Wire format of each AES call

For every 16-byte block: Each call takes ~10 ms including I²C overhead.

Padding

Each credential field (site, username, password) is up to 32 bytes in RAM. Before encryption:
  1. Trailing space characters (0x20) are replaced with 0xFF from the end inward.
  2. The field fills the 32-byte page buffer directly; any unused tail is 0xFF.
On decryption, bufferToString() strips 0xFF bytes and reads until \0 or 0xFF.

Per-operation flow

lock() — encrypt and write credentials

  1. Replace trailing spaces in currentSite, currentUser, currentPass with 0xFF.
  2. Load IV from EEPROM (loadIVfromEEPROM()).
  3. For each of the 3 fields:
    • Copy up to 32 bytes into the 32-byte buffer, padding any unused tail with 0xFF.
    • Call cbcEncrypt32(iv, plain, encrypted) — two ATECC AES calls under the hood.
    • Write the 32-byte ciphertext to the correct EEPROM page.

unlock() — decrypt and load credentials

  1. Load IV from EEPROM.
  2. Self-heal check: if slot 0 page 0 is raw 0xFF, call silentEraseAll().
  3. For each of the 3 fields:
    • Read the 32-byte ciphertext from EEPROM.
    • Call cbcDecrypt32(iv, encrypted, decrypted) — two ATECC AES calls.
    • Copy the 32 bytes into currentSite / currentUser / currentPass.

Error reporting

When an AES round-trip fails, the firmware preserves the chip’s response code and displays it on the OLED instead of a generic error. The format is AES E<n> RC<x> SS<XX> plus a second line LC=<x> LV=<x> KT=<n> showing the chip’s lock and key-type state at the moment of failure: RC is the driver-level code (-1 wake, -2 I²C, -3 CRC, -4 chip status error, -5 timeout). SS is the raw status byte from the chip (0x0F execution error, 0x03 parse, 0x07 self-test, …). The combination tells you exactly why the chip rejected the call. Locked + KT=1 (instead of 6) means the chip is permanently misconfigured for AES; that’s the only failure mode that can’t be cleared by a reboot.

Security considerations

The AES key cannot be rotated or recovered once the data zone is locked. If the secure element fails, every credential encrypted under it becomes unreadable. Use the USB-CDC backup command on a trusted host before relying on the device long-term.