Offline by design
ZeroKeyUSB does not rely on the Internet, cloud storage, or companion apps. Everything — from random number generation to PIN verification — happens inside the device, powered directly through USB. Your passwords never leave the hardware and cannot be accessed remotely, even by the manufacturer.Two cooperating chips
Security is split across two pieces of silicon so neither one alone can leak the vault:
The MCU and ATECC608A share an I²C bus at address
0x60. The MCU does not have a usable copy of the AES key: it sends 16-byte plaintext blocks to the chip and receives 16-byte ciphertext back. The chip generated the key itself at provisioning time using its TRNG; the byte sequence never crossed the I²C bus.
Crypto model — Camino A (AES key + AES engine on chip). The firmware enables the hardware AES command, configures slot 8 as an AES key holder (IsSecret=1,KeyType=6), writes a 16-byte TRNG-generated key to it, and locks both Config and Data zones. From that point on, encryption and decryption are single-block ECB calls to the chip, chained on the MCU into CBC.
Encryption architecture
All sensitive data is stored in the external EEPROM M24C64-WMN6TP, encrypted using AES-128 in CBC mode. Each ECB block is computed by the ATECC608A hardware AES engine; the MCU handles only the CBC XOR chaining.CBC chaining detail
cbcEncrypt32 / cbcDecrypt32 process each 32-byte credential in two 16-byte blocks. For each block:
- The plaintext block is XORed with the previous ciphertext (or with the device IV for the first block).
- The XOR result is sent to the ATECC608A via the AES command (
mode=0x00for encrypt,0x01for decrypt, key from slot 8, key block 0). The chip returns the 16-byte ciphertext. - The resulting ciphertext becomes
prevfor the next block.
AES command exposes only single-block ECB. CBC is the chaining policy layered on top — implementing it on the MCU keeps every byte of the key inside the secure element while giving us the diffusion benefits of CBC on the credential data.
EEPROM security map
Note on 0x0028. Older units (compiled before the AES move) used this region to hold the 16-byte AES master in plaintext. New units do not touch it; the bytes remain at whatever the EEPROM had previously. Treat the address as reserved.
The Master PIN
The PIN authorises an unlock cycle; it is never used directly as an encryption key. The verification flow:- On boot, before the PIN screen accepts input, the firmware replays the accumulated backoff for the stored failed-attempt count (
waitFromEeprom()). - The user enters up to 16 digits on the capacitive pads.
derivePinKey()computesSHA-256(pinArray[16] ∥ chip_serial[9])to produce the 32-byte PIN hash.- It reads the stored 32-byte hash from EEPROM (
0x0048) and runs a constant-time compare (diff |= stored[i] ^ derived[i]). - On match: the failed-attempt counter is cleared and the unlock proceeds.
- On mismatch: the counter increments and the device enforces an exponential backoff delay before the next attempt — and again at the next boot.
0x0002) and is re-applied on every power-up, so an attacker cannot skip the delay by cutting power. The vault is never wiped by wrong PINs. Note that this protects only guesses made through the device: an attacker who reads the PIN hash off the I²C bus can crack it offline with no delay, which is why the epoxy encapsulation (blocking bus access) and a long PIN matter.
Exponential backoff — delay schedule
Stored at EEPROM0x0002; re-applied at boot; reset on correct PIN.
Formula:
wait = BASE_SECONDS (5) × 2^(min(attempts,10)−1), capped at MAX_WAIT_SECONDS (2 560).
No destructive lockout
An earlier design used the ATECC608A’s monotonicCounter0 to wipe the vault after 50 wrong PINs. That mechanism was removed: verifySignature() no longer increments Counter0, reads any threshold, or calls eraseAll() on failed attempts. The persistent backoff above is the sole automatic brute-force defence; eraseAll() runs only on a user-initiated factory reset.
ATECC608A slot map
Established by the device itself the first time it boots, then locked permanently (Config and Data zones both irreversibly closed):Provisioning sequence (first boot only)
zerokeyAtecc.provisionAesAndLock() runs once, before the setup wizard:
- Read Config Zone blocks 0, 1 and 3 to learn the chip’s current factory values.
- Set the AES_Enable bit (byte 13, bit 0) using a 32-byte block write that preserves every factory bit we didn’t intend to change. Re-read and verify the bit took effect; abort without locking if not.
- Set SlotConfig[8] —
IsSecret=1(bit 7 of byte 36) andWriteConfig=Never(high nibble of byte 37 =0x4), preserving the rest of the byte. Re-read and verify. - Set KeyConfig[8].KeyType = 6 (AES) — bits 2..4 of byte 112, preserving every other bit. Re-read and verify.
- Lock the Config zone. Irreversible.
- Generate a 16-byte random key via the chip’s TRNG and write it to slot 8 in the clear (still allowed while the data zone is open).
- Lock the Data zone. Irreversible.
PROV E<n> error with the chip’s raw status byte attached and does not proceed to lock the zone, so a misbehaving chip cannot brick itself silently.
Why bit-level writes? The MAHDA-T parts ship with several “reserved” bits in byte 13 (AES_Enable) factory-set. A naive write that clears them is rejected by the chip with a parse error (SS=0x03). The provisioning code reads each byte first, OR-masks only the bits it actually needs to flip, and writes the whole 32-byte block back.
Known trade-off (Slot 9 readable): Slot 9 is not locked as secret because the MAHDA-T SKU rejects clear writes to IsSecret slots 0–7. The PIN hash lives there with IsSecret=0, so an attacker with physical I²C access can read the 32-byte hash and attempt an offline SHA-256(PIN∥serial) brute force. The persistent backoff limits online attempts but does nothing against offline cracking — the epoxy encapsulation (blocking bus access) is what stands in the way there. Short PINs are vulnerable to this attack — use the maximum 16 digits.
Initialization Vector
The IV is generated once during provisioning by the ATECC608A’s TRNG and stored in EEPROM at0x0010. Two sanity guards protect it:
- A read returning all-
0x00or all-0xFFis treated as uninitialised and triggers regeneration from the TRNG. - If EEPROM read fails at unlock time, the firmware attempts TRNG regeneration and re-stores the IV.
silentEraseAll) is called automatically on first unlock if slot 0 page 0 is still raw 0xFF (EEPROM default), and can be called again manually via generateAndStoreIV().
Self-healing initialisation
On the first unlock after provisioning,ZerokeySecurity::unlock() checks whether credential slot 0, page 0 is still at the EEPROM factory default (0xFF across all 32 bytes). If so, it calls silentEraseAll():
- Loads the device IV from EEPROM.
- For each of the 61 credential slots × 4 pages: encrypts a 32-byte
0xFFblank under AES-128 CBC and writes it to EEPROM. - Clears TOTP metadata for every slot.
Data segmentation
Each credential slot occupies 4 consecutive EEPROM pages (128 bytes total):
Splitting fields keeps recognisable plaintext patterns out of the ciphertext stream and limits the blast radius of a corrupt EEPROM page. Padding bytes are
0xFF; trailing 0x20 (space) chars are replaced with 0xFF before encryption to avoid pattern leakage.
Tamper protection
- The PCB is encapsulated in epoxy resin; opening the device destroys the board and the chip connections.
- No wireless interfaces (no Wi-Fi, no Bluetooth, no NFC).
- The bootloader region is BOOTPROT-locked in hardware fuses (
BOOTPROT = 7, protecting the first 16 KB) — application firmware cannot rewrite or relocate the bootloader. - The bootloader will only jump to ECDSA P-256-signed firmware; an unsigned or tampered image falls into USB-CDC recovery mode instead of executing.
- All decrypted credentials live in temporary RAM buffers (
currentSite,currentUser,currentPass) that are populated at unlock and overwritten at the next lock or power cycle. - Write-protect pin (
EEPROM_WP_PIN = PA01) can be driven high by firmware to hardware-lock EEPROM writes.
Backup and restore
Credentials can be exported and imported over the USB CDC serial interface:- Export (
backupAllCredentials): decrypts all 61 slots in-device and sends them as comma-delimited plaintext lines over SerialUSB. The host receives credentials in clear — ensure the USB connection is trusted. - Import (
loadAllbackupCredentials): receives records from the host, re-encrypts them under the current device IV/master, and writes them to EEPROM. TOTP secrets are parsed and stored in page 3 of each slot.
Security note: backup transmits decrypted credentials in plaintext over USB. Only perform backup/restore on a trusted, air-gapped host.
Known limitations and trade-offs
Transparency, not dependence
ZeroKeyUSB’s firmware is fully open-source and available for public audit and verification. Anyone can review:- How the ATECC608A is driven, including the bit-level provisioning routine that enables the chip’s AES engine and locks both zones (
zerokey-atecc.cpp). - How CBC chaining wraps the chip’s single-block AES command (
zerokey-security.cpp). - How the IV is generated, validated, and refreshed (
zerokey-security.cpp::generateAndStoreIV). - How the bootloader hashes and verifies the application (
bootloader/src/main.c).
Dive deeper
AES-128 Encryption
How the ATECC608A’s hardware AES engine encrypts every credential block, with CBC chaining wrapped around it on the MCU.
PIN Verification
The persistent backoff, the EEPROM-stored SHA-256 hash, and the constant-time compare that gates unlock.
IV Generation
How the ATECC608A TRNG seeds the device-wide IV and how regeneration is handled.
ZeroKeyUSB pairs an MCU with a hardened secure element. The chip provides the entropy (TRNG), the identity (chip serial used as PIN salt), and the cipher itself — every AES block is computed inside the secure element using a key the MCU has never seen. The MCU’s role is to chain those blocks into CBC, drive the UI, and shuttle plaintext / ciphertext to and from the EEPROM.