ZeroKeyUSB usa un PIN maestro (1–16 dígitos) para autenticar al usuario. El proceso de verificación combina una comparación software de hash a tiempo constante con un backoff exponencial persistente que se vuelve a aplicar en cada arranque, haciendo la fuerza bruta impracticable sin destruir jamás los datos guardados.
Cómo se guarda el PIN
El PIN nunca se guarda en texto plano. Al configurar el PIN (storeSignature()):
- Los dígitos del PIN del usuario se leen desde
pinArray[16] (cada byte contiene un valor de dígito 0–9).
derivePinKey() computa: SHA-256(pinArray[16] ∥ chip_serial[9]).
chip_serial es el serial único de 9 bytes leído desde la Config Zone del ATECC608A.
- El hash resultante de 32 bytes se escribe en EEPROM en
0x0048–0x0067.
- El mismo hash de 32 bytes también se escribe en el slot 9 del ATECC (para uso potencial futuro con CheckMac).
El serial del chip actúa como salt hardware: el mismo PIN numérico en otro dispositivo produce un hash de 32 bytes completamente distinto.
Secuencia de unlock
Cada intento de unlock ejecuta los siguientes pasos en verifySignature():
Al arrancar, antes de que la pantalla de PIN acepte ninguna entrada:
waitFromEeprom() // reaplica el backoff acumulado para el contador de fallos guardado
Luego cada intento de unlock ejecuta verifySignature():
1. derivePinKey(pinArray, derived):
serial = ATECC608A.readSerial()
derived = SHA-256(pinArray[16] || serial[9])
2. Leer hash guardado desde EEPROM [0x0048] → stored[32]
3. diff = 0; for i in 0..31: diff |= stored[i] ^ derived[i] // tiempo constante
4. Si diff == 0:
writeFailedAttemptsCounter(0) // limpia el backoff
→ ACCESO CONCEDIDO
5. Si no:
incrementFailedAttemptsCounter()
waitFromEeprom() // backoff exponencial
→ ACCESO DENEGADO
En esta ruta no hay incremento de Counter0, ni lectura de umbral, ni borrado automático. Un diseño anterior usaba el Counter0 monotónico del ATECC608A para borrar la bóveda tras 50 PINs incorrectos; eso se eliminó. La defensa real es el backoff persistente descrito abajo.
Rate-limiting persistente — la defensa real contra fuerza bruta
No hay borrado automático tras un número de intentos fallidos; la bóveda nunca se destruye por PINs incorrectos. En su lugar, cada intento se ralentiza con un backoff exponencial cuyo contador vive en EEPROM (0x0002) y por tanto sobrevive a la pérdida de alimentación.
El detalle clave es cuándo se aplica el retardo. En cada arranque, readConfigurationFlag() llama a waitFromEeprom() antes de que la pantalla de PIN acepte ninguna entrada. Así, un atacante no puede saltarse la penalización cortando la corriente a mitad de la cuenta atrás: tras cada intento fallido, el retardo acumulado se reimpone en el siguiente encendido. Una vez el contador supera ~10 fallos, cada intento adicional cuesta ≈ 43 minutos, de modo que la fuerza bruta online es impracticable (un PIN de 4 dígitos tardaría del orden de un año) — todo ello sin destruir jamás los datos del usuario.
| Evento | Contador de fallos (EEPROM 0x0002) |
|---|
| PIN incorrecto | +1, y luego aplica el retardo de backoff |
| PIN correcto | se resetea a 0 |
| Ciclo de alimentación | el retardo del contador guardado se reaplica al arrancar |
eraseAll() sigue existiendo, pero solo lo dispara manualmente el usuario (reset de fábrica / PIN olvidado) — nunca automáticamente por PINs incorrectos.
Salvedad offline. El rate-limit solo se aplica a los intentos hechos a través del dispositivo. El hash del PIN es legible por I²C (EEPROM 0x0048 y slot 9 del ATECC con IsSecret=0), así que un atacante que alcance físicamente el bus I²C puede copiar el hash y el serial del chip y crackear el PIN offline sin ningún retardo. Lo que lo impide es la encapsulación en resina que bloquea el acceso al bus — más usar un PIN largo. No es el backoff.
Backoff exponencial — tabla de retardos
Guardado en EEPROM 0x0002 y reaplicado al arrancar; se resetea solo con un PIN correcto:
| Intentos fallidos | Tiempo de espera |
|---|
| 0 | ninguno |
| 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) |
Fórmula: wait = 5 × 2^(min(intentos, 10) − 1) segundos, con tope en 2 560 s.
Durante el delay, el OLED muestra una barra de progreso y cuenta atrás. El dispositivo no acepta entrada nueva hasta que expire el temporizador.
Gestión segura de entrada
- Los dígitos se almacenan en
pinArray[16] en SRAM y se limpian tras la verificación.
- Los eventos táctiles se ignoran durante la espera de lockout (
waitFromEeprom()).
- SerialUSB no puede inyectar dígitos del PIN — solo se acepta entrada capacitiva táctil física.
- La comparación del PIN usa un acumulador XOR a tiempo constante (
diff |= stored[i] ^ derived[i]) para evitar timing side-channels.
Cambiar el PIN
Iniciado vía Menú → Change PIN → storeSignature():
- Cuenta atrás de 3 segundos en pantalla (permite aborto seguro).
derivePinKey(pinArray, derived) computa el nuevo hash.
- El nuevo hash de 32 bytes se escribe en el slot 9 del ATECC.
- El nuevo hash de 32 bytes se escribe en EEPROM
0x0048.
- Se limpia el contador de intentos fallidos (EEPROM
0x0002).
- El ping al ATECC confirma que el chip sigue vivo. La clave AES del slot 8 no se toca durante la configuración del PIN — se aprovisiona una vez al primer arranque y es irrevocable.
- El IV se carga o se genera.
- Se escribe el flag de config (
0x42).
- Todos los slots de credenciales se re-inicializan silenciosamente con blancos cifrados.
Cambiar el PIN no cambia la clave maestra AES ni re-cifra las credenciales existentes. La clave AES vive dentro del slot 8 del ATECC y se genera una vez por dispositivo; no se puede rotar. El ciphertext existente es descifrable con el mismo chip mientras no se destruya.
PIN olvidado
ZeroKeyUSB no tiene mecanismo de recuperación de PIN. La única opción es un reset de fábrica (eraseAll()), que:
- Muestra una cuenta atrás de 3 segundos.
- Carga el IV del dispositivo.
- Sobrescribe los 61 slots de credenciales × 4 páginas con blancos cifrados.
- Limpia los metadatos TOTP.
Tras el reset el dispositivo se para con un error “LOCKED — reflash”. Hay que usar el bootloader para flashear firmware nuevo y re-aprovisionar el dispositivo desde cero.
Las credenciales previamente almacenadas son irrecuperables a menos que tengas un backup en texto plano exportado antes del reset.
Elige un PIN que puedas recordar pero que otros no puedan adivinar. Un PIN de 4 dígitos o menos es vulnerable a ataques de diccionario SHA-256 offline si un adversario obtiene acceso I²C al dispositivo.