9.5 KiB
Idea de producto: el almacén cifrado de la app móvil y el PIN de coacción
6 de octubre de 2026. Idea del autor, discutida en una sesión de diseño. No forma parte del protocolo de la cápsula: es cómo guarda la app de Android e iOS sus cápsulas, sus claves y sus datos. El modo coacción sirve al interruptor de liberación condicional.
Qué es
La app guarda todo en un único fichero, el almacén, que hace de directorio virtual. Sin el PIN y sin el móvil, el fichero no se distingue de bytes aleatorios: no tiene firma de formato, ni ficheros sueltos, ni nombres, ni tamaños por cápsula. Fuera del almacén no se guarda ninguna clave.
Se entra con un PIN numérico. Ese mismo PIN, escrito al revés, abre la app en modo coacción.
Por qué el PIN no es la clave
Un PIN numérico tiene muy poca entropía. Si fuera la clave, quien se llevara el fichero (de una copia de seguridad, por extracción forense o con root) lo rompería fuera del móvil:
| Secreto | Combinaciones | A 1.000 intentos/s | A 1.000.000 intentos/s |
|---|---|---|---|
| PIN de 6 dígitos | 10⁶ | 8 minutos | menos de 1 s |
| PIN de 8 dígitos | 10⁸ | 14 horas | 1 minuto |
Los tiempos son medios: de media se acierta tras probar la mitad de las combinaciones. Ningún KDF lo arregla. Por eso hay tres capas separadas:
| Capa | Qué es | Dónde está |
|---|---|---|
| PIN | Abre la app y da la señal de coacción | Solo en la cabeza del usuario |
| Clave de envoltura | Se deriva del PIN y de una clave del chip del móvil. Se calcula al abrir y se borra de memoria al cerrar | En ningún sitio |
| Clave de datos | 256 bits aleatorios, generados al crear el almacén. Cifra el índice y los bloques | En la cabecera, cifrada con la clave de envoltura |
Separarlas da tres cosas:
- cambiar el PIN supone recifrar 32 bytes, no el almacén entero;
- la clave de datos tiene 256 bits de verdad, sea cual sea el PIN;
- puede haber una segunda envoltura, la de recuperación, sin tocar los datos.
La clave de envoltura
k_pin = scrypt(PIN, sal)
k_hw = secreto del chip, calculado sobre la sal
K = HKDF-SHA256(k_pin || k_hw, info = "datekeys-almacen-v1")
El secreto del chip sale de una clave no extraíble: el chip calcula con ella, pero no la entrega.
- Android: una clave HMAC-SHA256 del Keystore, en StrongBox si el móvil lo tiene y, si no, en el entorno seguro del procesador (TEE).
k_hw = HMAC(clave_hw, sal). - iOS: el Secure Enclave no hace HMAC, solo P-256. Una opción es el ECDH de la clave del enclave con su propia clave pública, pasado por HKDF con la sal. No hace falta guardar ningún punto. Está por confirmar.
- En los dos casos: accesible solo con el móvil desbloqueado, solo en ese dispositivo y sin pedir biometría, porque si no cada intento abriría un diálogo del sistema.
Lo que consigue. El fichero robado no se puede atacar fuera del móvil. Con el móvil y root, el atacante tiene que probar dentro de él, a un scrypt por intento (unos 1 s): un PIN de 6 dígitos cae en unos 6 días de media y uno de 8, en unos 1,6 años. Una app no puede pedirle al chip que limite los intentos de su propio PIN (eso solo existe para el bloqueo del sistema), así que ese es el techo. La app añade además esperas crecientes tras cada fallo. Eso para a quien no tiene root, no a quien lo tiene.
PIN: de 6 dígitos como mínimo, y con la opción de usar 8.
Lo que cuesta. El almacén no se puede llevar a otro móvil: la clave del chip no sale de él. Para eso está la ranura de recuperación.
La recuperación
Una segunda envoltura de la misma clave de datos, cifrada con 5 palabras generadas por la app. Salen de la lista española de BIP39, de 2048 palabras: las 4 primeras letras de cada una no se repiten, así que se escriben con autocompletado. El usuario las apunta en papel.
- La clave sale solo de
scrypt(palabras, sal), sin el chip, para que sirva en otro móvil. - 5 palabras dan unas 2⁵⁵ combinaciones: unos 570 años de media a un millón de intentos por segundo.
- Es el punto más débil frente a un fichero robado, porque se ataca fuera del móvil. Por eso son 5 palabras y no 4.
- Es opcional. Si el usuario no la configura, la ranura se rellena con bytes aleatorios y perder el móvil supone perderlo todo.
El fichero
[ sal, 32 B ]
[ cabecera A ][ cabecera B ]
[ bloques de datos, de tamaño fijo ]
Cada cabecera tiene tamaño fijo y contiene:
- la ranura 1: la clave de datos cifrada con
K, del PIN y el chip; - la ranura 2: la clave de datos cifrada con la llave de recuperación, o bytes aleatorios;
- el índice, cifrado con la clave de datos: la versión, el contador de escrituras y la tabla del directorio (nombres, tamaños y qué bloques ocupa cada entrada).
Los bloques, por ejemplo de 4 KiB, se cifran cada uno con ChaCha20-Poly1305 y la clave de datos. Cada bloque lleva su propio nonce aleatorio. Como datos asociados lleva su número de bloque y el contador de escrituras, para que nadie pueda intercambiar bloques ni reponer uno antiguo suelto. Los bloques libres se rellenan con bytes aleatorios: no se ve qué está ocupado.
No se revela nada en claro. No hay firma de formato ni versión legible. Los parámetros de scrypt los fija la versión de la app y, si cambian, la app migra el almacén. Siempre hay dos ranuras, estén en uso o no.
El tamaño. El almacén se crea con un tamaño fijo, por ejemplo 16 MiB, y crece duplicándose. Si creciera con cada cápsula, el tamaño diría cuántas hay.
Escritura atómica. Los datos nuevos se escriben en bloques libres y nunca encima de los que están en uso. Después se escribe la cabecera que no está en uso, con el contador incrementado. Al abrir, gana la cabecera válida con el contador más alto. Un corte a mitad de una escritura deja la versión anterior intacta.
Fuera de las copias de seguridad. En Android, allowBackup=false o dataExtractionRules. En iOS, excluido de la copia de iCloud y con protección de fichero NSFileProtectionComplete, o CompleteUnlessOpen si la app escribe en segundo plano.
El PIN de coacción
Cómo se detecta sin guardar nada:
- Se prueba la ranura 1 con el PIN introducido. Si abre, es la entrada normal.
- Si no, se prueba con el PIN al revés. Si abre, es coacción.
- Si tampoco abre, el PIN es incorrecto. Un PIN incorrecto cuesta dos scrypt, unos 2 s.
Regla obligatoria: se prohíben los PIN capicúa (1221, 123321, 000000), porque al revés son el mismo y la coacción no se distinguiría.
El bloqueo de 48 horas lo hace cumplir el interruptor, no el móvil. Si solo viviera en el móvil, quien coacciona lo saltaría:
- reponiendo una copia anterior del fichero, de antes de la marca de bloqueo (no hay un contador monótono de hardware al alcance de una app);
- cambiando la hora del sistema, o reiniciando para poner a cero el reloj de tiempo transcurrido.
Por eso el modo coacción envía una señal al contrato del interruptor, firmada por el propietario, y es el contrato el que rechaza durante 48 horas cualquier renovación o cancelación. En el móvil, el bloqueo es solo la cara visible.
Cortar la red no sirve de nada. Si quien coacciona impide que la señal salga, nadie renueva el interruptor y su cuenta atrás sigue hasta la liberación. Es la propiedad básica de un interruptor de hombre muerto.
Bloqueo visible o modo silencioso.
- Visible: la app muestra "bloqueado 48 horas". Quien coacciona sabe en ese momento que lo han engañado, y eso pone en riesgo al usuario delante de él.
- Silencioso: la app parece funcionar con normalidad y muestra, por ejemplo, "renovación hecha", pero al contrato le llega la señal de coacción y no se renueva nada. Es lo habitual en los sistemas con PIN de coacción, y es lo recomendado.
Dependencias
- Cifrado: ChaCha20-Poly1305, scrypt y HKDF-SHA256 son las primitivas que ya usa age. No hace falta ningún paquete nuevo.
- Keystore y Keychain: código de plataforma propio, unas 100 líneas de Kotlin y otras tantas de Swift (crear la clave del chip y calcular
k_hw). La alternativa es un paquete nuevo, comoflutter_secure_storage, que necesitaría aprobación y además no da control sobre StrongBox ni sobre el ECDH del enclave. - Lista de palabras: la lista española de BIP39 es un fichero de datos con licencia MIT, no una dependencia.
Riesgos
- PIN olvidado y sin recuperación: se pierde todo. No hay ningún otro camino, porque no se guarda nada.
- Móvil con root en manos de un atacante paciente: un PIN de 6 dígitos cae en días. Lo único que protege más es el PIN de 8 dígitos.
- Las palabras de recuperación son la vía de ataque al fichero robado. Si están en papel, quien encuentre el papel tiene el almacén.
- Lo que no se oculta: que la app está instalada y que el almacén existe, con su tamaño redondeado.
- iOS: falta confirmar que el ECDH del Secure Enclave consigo mismo es un buen secreto de dispositivo y que no hay una vía mejor.
- Las primitivas de Dart: hay que medir scrypt en móviles de gama baja para fijar los parámetros, apuntando a unos 1 s.
Preguntas abiertas
- ¿Modo coacción visible o silencioso?
- ¿Qué hace exactamente el contrato con la señal de coacción: rechazar renovaciones durante 48 horas, adelantar la liberación, avisar a los custodios?
- ¿Recuperación con 5 palabras, o se acepta perderlo todo al perder el móvil?
- ¿Tamaño inicial del almacén? ¿Guarda también las cápsulas grandes, o solo las claves y las pequeñas?
- ¿Varios intentos fallidos seguidos activan el modo coacción, o solo las esperas crecientes?