ZeroKeyUSB uses a Master PIN (1–16 digits) to authenticate the user. The verification process combines a software constant-time hash comparison with a hardware monotonic counter in the ATECC608A to block brute-force attempts at both software and hardware levels.
How the PIN is stored
The PIN is never stored in plaintext. At PIN setup time (storeSignature()):
- The user’s PIN digits are read from
pinArray[16] (each byte holds a digit value 0–9).
derivePinKey() computes: SHA-256(pinArray[16] ∥ chip_serial[9]).
chip_serial is the 9-byte unique serial read from the ATECC608A Config Zone.
- The resulting 32-byte hash is written to EEPROM at
0x0048–0x0067.
- The same 32-byte hash is also written to ATECC slot 9 (for potential future CheckMac use).
The chip serial acts as a hardware salt: the same numeric PIN on a different device produces a completely different 32-byte hash.
Unlock sequence
Each unlock attempt executes the following steps in verifySignature():
1. zerokeyAtecc.counterIncrement(Counter0) → new_counter
2. Read threshold from EEPROM [0x0020]
3. If new_counter >= threshold → eraseAll() + halt (hard lockout)
4. derivePinKey(pinArray, derived):
serial = ATECC608A.readSerial()
derived = SHA-256(pinArray[16] || serial[9])
5. Read stored hash from EEPROM [0x0048] → stored[32]
6. diff = 0; for i in 0..31: diff |= stored[i] ^ derived[i] // constant-time
7. If diff == 0:
writeThreshold(new_counter + 50) // reset budget
writeFailedAttemptsCounter(0)
→ ACCESS GRANTED
8. Else:
incrementFailedAttemptsCounter()
waitFromEeprom() // exponential backoff
→ ACCESS DENIED
Hardware counter — hard limit
Counter0 in the ATECC608A is a monotonic hardware counter. Its key properties:
- Increments by 1 on every
counterIncrement() call — once per PIN attempt.
- Cannot be decremented, reset, or rolled back by any software, power cycle, or hardware reset.
- Persists across power loss inside the chip’s non-volatile memory.
- The threshold stored at EEPROM
0x0020 is a uint32 (little-endian) representing the counter value at which the next wipe occurs.
| Event | Counter effect | Threshold effect |
|---|
| Wrong PIN attempt | +1 | unchanged |
| Correct PIN | +1 | reset to Counter0 + 50 |
| Counter ≥ threshold | — | eraseAll() triggered |
With an uninterrupted stream of wrong PINs the counter catches up to the threshold in exactly 50 attempts and triggers a full credential wipe.
Adaptive delays — soft backoff
Stored at EEPROM 0x0002. This UX-layer counter is reset on a correct PIN:
| Failed attempts | Wait time |
|---|
| 0 | none |
| 1 | 5 s |
| 2 | 10 s |
| 3 | 20 s |
| 4 | 40 s |
| 5 | 80 s |
| 6 | 160 s |
| 7 | 320 s |
| 8 | 640 s |
| 9 | 1 280 s |
| ≥ 10 | 2 560 s (≈ 43 min) |
Formula: wait = 5 × 2^(min(attempts, 10) − 1) seconds, capped at 2 560 s.
During the delay the OLED shows a progress bar and countdown. The device does not accept new input until the timer expires.
- Digits are buffered in
pinArray[16] in SRAM and cleared after verification.
- Touch events are ignored during the lockout wait (
waitFromEeprom()).
- SerialUSB cannot inject PIN digits — only physical capacitive-touch input is accepted.
- The PIN comparison uses a constant-time XOR accumulator (
diff |= stored[i] ^ derived[i]) to avoid timing side-channels.
Changing the PIN
Initiated via Menu → Change PIN → storeSignature():
- 3-second on-screen countdown (allows safe abort).
derivePinKey(pinArray, derived) computes the new hash.
- New 32-byte hash written to ATECC slot 9.
- New 32-byte hash written to EEPROM
0x0048.
- Current Counter0 is read; threshold is set to
Counter0 + 50.
- ATECC ping confirms the chip is still alive. The AES key in slot 8 is not touched by PIN setup — it is provisioned once at first boot and is irrevocable.
- IV is loaded or generated.
- Setup config flag is written (
0x42).
- All credential slots are silently re-initialised with encrypted blanks.
Changing the PIN does not change the AES master key or re-encrypt existing credentials. The AES key lives inside ATECC slot 8 and is generated once per device; it cannot be rotated. Existing ciphertext is decryptable with the same chip as long as it is not destroyed.
Forgotten PIN
ZeroKeyUSB has no PIN recovery mechanism. The only option is a factory reset (eraseAll()), which:
- Shows a 3-second countdown.
- Loads the device IV.
- Overwrites all 62 credential slots × 4 pages with encrypted blanks.
- Clears TOTP metadata.
After reset the device halts with a “LOCKED — reflash” error. The bootloader must be used to flash new firmware and re-provision the device from scratch.
Previously stored credentials are unrecoverable unless you have a plaintext backup exported before the reset.
Choose a PIN you can remember but others cannot guess. A PIN of 4 digits or fewer is vulnerable to offline SHA-256 dictionary attacks if an adversary gains I²C access to the device.