Skip to main content

Offline por diseño

ZeroKeyUSB no depende de Internet, almacenamiento en la nube ni apps acompañantes. Todo — desde la generación de números aleatorios hasta la verificación del PIN — ocurre dentro del dispositivo, alimentado directamente por USB. Tus contraseñas nunca salen del hardware y no se pueden acceder remotamente, ni siquiera por el fabricante.

Dos chips cooperando

La seguridad está dividida entre dos piezas de silicio para que ninguna por sí sola pueda filtrar la bóveda: El MCU y el ATECC608A comparten un bus I²C en la dirección 0x60. El MCU no tiene una copia usable de la clave AES: envía bloques de plaintext de 16 bytes al chip y recibe 16 bytes de ciphertext de vuelta. El chip generó la clave por sí mismo al aprovisionarse usando su TRNG; la secuencia de bytes nunca cruzó el bus I²C.
Modelo criptográfico — Camino A (clave AES + motor AES en chip). El firmware habilita el comando AES hardware, configura el slot 8 como contenedor de clave AES (IsSecret=1, KeyType=6), escribe una clave de 16 bytes generada por el TRNG, y bloquea tanto la zona Config como Data. A partir de ahí, el cifrado y descifrado son llamadas ECB de bloque único al chip, encadenadas en el MCU en CBC.

Arquitectura de cifrado

Todos los datos sensibles se almacenan en la EEPROM externa M24C64-WMN6TP, cifrados con AES-128 en modo CBC. Cada bloque ECB lo computa el motor AES hardware del ATECC608A; el MCU solo gestiona el encadenamiento XOR del CBC.

Detalle del encadenamiento CBC

cbcEncrypt32 / cbcDecrypt32 procesan cada credencial de 32 bytes en dos bloques de 16 bytes. Para cada bloque:
  1. El bloque de plaintext se XOR con el ciphertext anterior (o con el IV del dispositivo para el primer bloque).
  2. El resultado del XOR se envía al ATECC608A vía el comando AES (mode=0x00 para encrypt, 0x01 para decrypt, clave del slot 8, key block 0). El chip devuelve los 16 bytes de ciphertext.
  3. El ciphertext resultante se convierte en prev para el siguiente bloque.
El descifrado es simétrico: el chip devuelve plaintext, el MCU lo XOR con el ciphertext anterior para recuperar el bloque original. ¿Por qué dividir el trabajo así? El comando AES del ATECC608A solo expone ECB de bloque único. CBC es la política de encadenamiento añadida encima — implementarlo en el MCU mantiene cada byte de la clave dentro del elemento seguro mientras nos da los beneficios de difusión de CBC en los datos de credenciales.

Mapa de seguridad de la EEPROM

Nota sobre 0x0028. Unidades antiguas (compiladas antes del cambio de AES) usaban esta región para guardar la clave maestra AES de 16 bytes en plaintext. Las unidades nuevas no la tocan; los bytes permanecen con lo que tuviera la EEPROM. Trata la dirección como reservada.

El PIN maestro

El PIN autoriza un ciclo de unlock; nunca se usa directamente como clave de cifrado. El flujo de verificación:
  1. Al arrancar, antes de que la pantalla de PIN acepte entrada, el firmware reaplica el backoff acumulado para el contador de fallos guardado (waitFromEeprom()).
  2. El usuario introduce hasta 16 dígitos en los pads capacitivos.
  3. derivePinKey() computa SHA-256(pinArray[16] ∥ chip_serial[9]) para producir el hash de 32 bytes.
  4. Lee el hash guardado de 32 bytes desde EEPROM (0x0048) y ejecuta una comparación a tiempo constante (diff |= stored[i] ^ derived[i]).
  5. En coincidencia: el contador de fallos se limpia y el unlock procede.
  6. En no coincidencia: el contador se incrementa y el dispositivo aplica un delay exponencial antes del siguiente intento — y de nuevo en el siguiente arranque.
El contador de backoff vive en EEPROM (0x0002) y se reaplica en cada encendido, así que un atacante no puede saltarse el retardo cortando la corriente. La bóveda nunca se borra por PINs incorrectos. Ten en cuenta que esto solo protege los intentos hechos a través del dispositivo: un atacante que lea el hash del PIN por el bus I²C puede crackearlo offline sin ningún retardo, por eso importan la encapsulación en resina (que bloquea el acceso al bus) y un PIN largo.

Backoff exponencial — tabla de retardos

Guardado en EEPROM 0x0002; reaplicado al arrancar; se resetea con un PIN correcto. Fórmula: wait = BASE_SECONDS (5) × 2^(min(intentos,10)−1), con tope en MAX_WAIT_SECONDS (2 560).

Sin bloqueo destructivo

Un diseño anterior usaba el Counter0 monotónico del ATECC608A para borrar la bóveda tras 50 PINs incorrectos. Ese mecanismo se eliminó: verifySignature() ya no incrementa Counter0, ni lee ningún umbral, ni llama a eraseAll() en intentos fallidos. El backoff persistente de arriba es la única defensa automática contra fuerza bruta; eraseAll() solo corre en un reset de fábrica iniciado por el usuario.

Mapa de slots del ATECC608A

Establecido por el propio dispositivo la primera vez que arranca, luego bloqueado permanentemente (las zonas Config y Data ambas cerradas irreversiblemente):

Secuencia de aprovisionamiento (solo primer arranque)

zerokeyAtecc.provisionAesAndLock() corre una vez, antes del asistente de setup:
  1. Lee los bloques 0, 1 y 3 de la Config Zone para conocer los valores de fábrica actuales del chip.
  2. Establece el bit AES_Enable (byte 13, bit 0) usando una escritura de bloque de 32 bytes que preserva cada bit de fábrica que no pretendíamos cambiar. Re-lee y verifica que el bit tuvo efecto; aborta sin bloquear si no.
  3. Establece SlotConfig[8] — IsSecret=1 (bit 7 del byte 36) y WriteConfig=Never (nibble alto del byte 37 = 0x4), preservando el resto del byte. Re-lee y verifica.
  4. Establece KeyConfig[8].KeyType = 6 (AES) — bits 2..4 del byte 112, preservando cada otro bit. Re-lee y verifica.
  5. Bloquea la zona Config. Irreversible.
  6. Genera una clave aleatoria de 16 bytes vía el TRNG del chip y la escribe en el slot 8 en claro (aún permitido mientras la zona de datos esté abierta).
  7. Bloquea la zona Data. Irreversible.
Cada escritura va seguida de una re-lectura. Si algún verify falla, la función devuelve un error numerado PROV E<n> con el byte de status crudo del chip adjunto y no procede a bloquear la zona, de modo que un chip que se porte mal no pueda brickearse silenciosamente.
¿Por qué escrituras a nivel de bit? Los chips MAHDA-T se entregan con varios bits “reservados” en el byte 13 (AES_Enable) configurados de fábrica. Una escritura ingenua que los borra es rechazada por el chip con un parse error (SS=0x03). El código de aprovisionamiento lee cada byte primero, hace OR-mask solo de los bits que necesita cambiar, y escribe el bloque de 32 bytes entero de vuelta.
Trade-off conocido (Slot 9 legible): El slot 9 no está bloqueado como secreto porque el SKU MAHDA-T rechaza escrituras en claro a slots IsSecret 0–7. El hash del PIN vive ahí con IsSecret=0, así que un atacante con acceso físico I²C puede leer el hash de 32 bytes e intentar fuerza bruta offline SHA-256(PIN∥serial). El backoff persistente limita los intentos online pero no hace nada contra el cracking offline — ahí lo que se interpone es la encapsulación en resina (que bloquea el acceso al bus). Los PINs cortos son vulnerables a este ataque — usa los 16 dígitos máximos.

Vector de inicialización

El IV se genera una vez durante el aprovisionamiento por el TRNG del ATECC608A y se guarda en EEPROM en 0x0010. Dos comprobaciones lo protegen:
  • Una lectura que devuelva todo 0x00 o todo 0xFF se trata como sin inicializar y dispara regeneración desde el TRNG.
  • Si la lectura de EEPROM falla en el momento del unlock, el firmware intenta regenerar desde el TRNG y re-guarda el IV.
IV único por dispositivo: todas las páginas de credenciales se encadenan contra el mismo IV. Esto mantiene el layout simple y auditable. El modelo de amenaza se apoya en la calidad del TRNG y en el secreto de la clave AES, no en nonces por registro. Consecuencia de la regeneración: si el IV se pierde o se regenera sin re-cifrar las credenciales, los slots existentes descifrarán a basura (el ciphertext se produjo bajo el IV antiguo). La rutina de auto-curación del firmware (silentEraseAll) se llama automáticamente en el primer unlock si el slot 0 página 0 sigue en 0xFF (por defecto de EEPROM), y se puede llamar de nuevo manualmente vía generateAndStoreIV().

Inicialización con auto-curación

En el primer unlock tras el aprovisionamiento, ZerokeySecurity::unlock() comprueba si el slot 0 página 0 de credenciales sigue en el valor por defecto de fábrica de EEPROM (0xFF en los 32 bytes). Si es así, llama a silentEraseAll():
  1. Carga el IV del dispositivo desde EEPROM.
  2. Para cada uno de los 61 slots × 4 páginas: cifra un blanco de 32 bytes 0xFF bajo AES-128 CBC y lo escribe en EEPROM.
  3. Limpia los metadatos TOTP de cada slot.
Esto garantiza que las unidades nuevas siempre tengan entradas en blanco consistentes y correctamente cifradas antes de que se escriba ninguna credencial.

Segmentación de datos

Cada slot de credencial ocupa 4 páginas EEPROM consecutivas (128 bytes en total): Dividir los campos mantiene patrones reconocibles de plaintext fuera del stream de ciphertext y limita el radio de explosión de una página EEPROM corrupta. Los bytes de padding son 0xFF; los espacios finales 0x20 se reemplazan con 0xFF antes del cifrado para evitar fugas de patrones.

Protección contra manipulación

  • El PCB está encapsulado en resina epoxy; abrir el dispositivo destruye la placa y las conexiones de los chips.
  • Sin interfaces wireless (sin Wi-Fi, sin Bluetooth, sin NFC).
  • La región del bootloader está BOOTPROT-locked en fuses hardware (BOOTPROT = 7, protegiendo los primeros 16 KB) — el firmware de aplicación no puede reescribir ni reubicar el bootloader.
  • El bootloader solo saltará a firmware firmado con ECDSA P-256; una imagen sin firmar o manipulada cae en modo de recuperación USB-CDC en vez de ejecutarse.
  • Todas las credenciales descifradas viven en buffers temporales de RAM (currentSite, currentUser, currentPass) que se rellenan al unlock y se sobrescriben en el siguiente lock o ciclo de alimentación.
  • El pin de Write Protect (EEPROM_WP_PIN = PA01) puede ponerse a alto por firmware para bloquear escrituras EEPROM por hardware.

Backup y restore

Las credenciales se pueden exportar e importar por la interfaz serie USB CDC:
  • Export (backupAllCredentials): descifra los 61 slots dentro del dispositivo y los envía como líneas plaintext separadas por comas por SerialUSB. El host recibe las credenciales en claro — asegúrate de que la conexión USB es de confianza.
  • Import (loadAllbackupCredentials): recibe registros desde el host, los re-cifra bajo el IV/clave maestra actual del dispositivo y los escribe en EEPROM. Los secretos TOTP se parsean y guardan en la página 3 de cada slot.
Nota de seguridad: el backup transmite credenciales descifradas en plaintext por USB. Solo realiza backup/restore en un host de confianza, air-gapped.

Limitaciones conocidas y compromisos


Transparencia, no dependencia

El firmware de ZeroKeyUSB es totalmente open source y está disponible para auditoría y verificación públicas. Cualquiera puede revisar:
  • Cómo se drivea el ATECC608A, incluyendo la rutina de aprovisionamiento bit-level que habilita el motor AES del chip y bloquea ambas zonas (zerokey-atecc.cpp).
  • Cómo el encadenamiento CBC envuelve el comando AES de bloque único del chip (zerokey-security.cpp).
  • Cómo se genera, valida y refresca el IV (zerokey-security.cpp::generateAndStoreIV).
  • Cómo el bootloader hashea y verifica la aplicación (bootloader/src/main.c).
No hay mecanismos de actualización remota: reflashear requiere acceso físico vía pogo pins SWD o el bootloader local USB-CDC, y cualquier nueva imagen debe estar firmada por la clave ECDSA offline.

Profundizar

Cifrado AES-128

Cómo el motor AES hardware del ATECC608A cifra cada bloque de credencial, con encadenamiento CBC envolviéndolo en el MCU.

Verificación del PIN

El backoff persistente, el hash SHA-256 guardado en EEPROM y la comparación a tiempo constante que gobierna el unlock.

Generación del IV

Cómo el TRNG del ATECC608A siembra el IV global del dispositivo y cómo se gestiona la regeneración.
ZeroKeyUSB combina un MCU con un elemento seguro endurecido. El chip proporciona la entropía (TRNG), la identidad (serial del chip usado como salt del PIN), y el cifrador en sí — cada bloque AES se computa dentro del elemento seguro usando una clave que el MCU nunca ha visto. El papel del MCU es encadenar esos bloques en CBC, manejar la UI y transportar plaintext / ciphertext hacia y desde la EEPROM.