Almacenamiento de claves y autenticación
BitTool Signer guarda la frase de recuperación de cada cartera (y la passphrase BIP39 o frase de contraseña adicional, si se ha definido) en su propio archivo cifrado. Para abrirlo hacen falta dos claves independientes: una derivada de la contraseña de la cartera y otra que iOS guarda solo en ese dispositivo.
Esta página describe la app de iPhone. BitTool Signer para Android usa las mismas dos capas con el almacenamiento de claves propio de Android; consulta En Android.
La bóveda
Cada cartera es un archivo Vaults/<UUID>.bsvault dentro del directorio privado Library/Application Support/BitToolSigner/ de la app. Se crea así:
- Se genera una sal nueva de 32 bytes con
SecRandomCopyBytes. - Se deriva una clave de contraseña de 256 bits: PBKDF2-HMAC-SHA256, 600 000 iteraciones (CommonCrypto).
- Se cifra el secreto (frase + passphrase, en JSON) con AES-256-GCM bajo la clave de contraseña: nonce nuevo de 96 bits, tag de 128 bits.
- Se vuelve a cifrar ese texto cifrado con AES-256-GCM bajo una clave de dispositivo aleatoria de 256 bits.
- Se guarda la clave de dispositivo en el Keychain con
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnlyy la sincronización con iCloud desactivada. Si el móvil no tiene código de acceso, la creación de la cartera falla; no hay ninguna alternativa más débil. - Se escribe el archivo de forma atómica con protección de archivos completa, permisos solo para el propietario y exclusión de las copias de seguridad.
vault file
├── header (not secret, but authenticated)
│ format version 2 · KDF parameters · salt · wallet ID · name · created at
└── AES-GCM(device key,
AES-GCM(PBKDF2-SHA256(password, salt, 600 000),
{ mnemonic, passphrase }))
Las dos capas AES-GCM autentican los bytes de la cabecera almacenada como datos asociados, precedidos de una etiqueta de dominio (com.bittool.signer.vault.v2/<layer>). Si se edita, se vuelve a codificar o se reordena cualquier byte de la cabecera, el descifrado falla.
La contraseña de la cartera cifra la bóveda local. No es la passphrase BIP39 y no influye en las direcciones. Reglas: de 8 a 64 caracteres, al menos tres de estos tipos: mayúsculas, minúsculas, dígitos y símbolos; sin caracteres de control; se usa exactamente tal como se escribe (sin recortar espacios ni normalizar).
La clave de dispositivo nunca sale del móvil y está excluida de las copias de seguridad, así que un archivo de bóveda copiado no se puede abrir en otro sitio. Perder el móvil, eliminar la app o quitar el código de acceso puede dejar la bóveda ilegible. La frase de recuperación escrita (y la passphrase) es la única copia de seguridad.
¿Por qué PBKDF2 y no Argon2id?
Argon2id es memory-hard (exigente en memoria) y sería una KDF de contraseñas más robusta. Esta versión usa el PBKDF2 de CommonCrypto (con la recomendación actual de OWASP de 600 000 iteraciones para HMAC-SHA256) para no añadir una dependencia criptográfica nativa. Los parámetros de la KDF están en la cabecera versionada, así que una futura migración no necesitará una nueva estructura de archivo.
La revisión de seguridad de Opus 5 clasificó esta elección como Informativa (BTS-OP5-03), no como una debilidad: debido a la segunda capa bajo la clave de dispositivo, alguien que copie solo el archivo de la bóveda ni siquiera puede empezar a probar contraseñas. Sugiere Argon2id para un futuro formato de bóveda, como defensa en profundidad por si la clave de dispositivo también se ve comprometida.
Intentos de contraseña
Los intentos fallidos de contraseña se cuentan por cartera en un archivo .attempts cifrado y autenticado. El contador se guarda antes de la derivación lenta de la clave, así que cerrar la app a la fuerza no lo reinicia.
| Intentos fallidos | Espera antes del siguiente intento |
|---|---|
| 1–4 | ninguna |
| 5 | 2 s |
| 6 | 4 s |
| n | min(2n−4 s, 5 min) |
Una contraseña correcta reinicia el contador. La app nunca elimina una cartera por introducir contraseñas incorrectas. Es un control de la aplicación, no un contador por hardware: alguien que pueda restaurar una versión anterior de los archivos de la app puede reiniciarlo. La protección criptográfica es la KDF.
Datos públicos sin la contraseña
Las direcciones, los códigos QR de vinculación y la actividad están disponibles sin la contraseña, porque solo necesitan claves públicas. Cuando se crea o se importa una cartera, el motor deriva una vez un índice público y lo guarda fuera de la bóveda:
- seis nodos de cuenta de Bitcoin (BIP84, BIP86, BIP49, BIP44, y multifirma BIP48 nativa y anidada)
- los nodos de cuenta de EVM y TRON, más las direcciones de TRON 0–99
- las tres claves públicas de Solana (consulta Rutas de derivación)
El archivo del índice público lleva un HMAC-SHA256 calculado con otra clave aleatoria del Keychain. Si no supera la verificación (alguien lo ha editado, o pertenece a otra cartera), la app lo trata como inexistente y pide la contraseña una vez para reconstruirlo. Por tanto, un atacante que pueda escribir en los archivos de la app no puede sustituir las direcciones por las suyas.
Autorización
- No existe un estado «desbloqueado» ni una sesión global. Cada tarea de firma pide la contraseña de esa cartera, y también la piden mostrar la frase de recuperación, cambiar la contraseña y eliminar la cartera.
- App Lock (bloqueo de la app; opcional, con Face ID) protege solo el acceso a la app. Nunca autoriza una firma. Con App Lock activado, la app se bloquea al iniciarse en frío y tras 120 segundos en segundo plano, medidos con un reloj que sigue contando mientras el móvil está en reposo.
- Cuando la app pasa a segundo plano, descarta de inmediato cualquier contraseña introducida, cualquier aceptación de riesgos y cualquier frase de recuperación que esté en pantalla. Una pantalla de privacidad oculta el contenido en el selector de apps y durante la grabación de pantalla.
Higiene de la memoria
La semilla BIP39 se calcula en código nativo y los búferes temporales se sobrescriben con memset_s. Cada operación que toca un secreto o una entrada no fiable se ejecuta en un contexto de JavaScriptCore nuevo que se descarta después. Así se acorta el tiempo que los secretos permanecen en memoria, pero no se puede garantizar que las cadenas de Swift o de JavaScript se borren, por lo que la app no afirma que la puesta a cero sea completa.
En Android
La bóveda tiene las mismas dos capas y el mismo encabezado autenticado. Lo que cambia es dónde vive la clave del dispositivo y cómo protege la plataforma a la app.
- Capa de la contraseña. PBKDF2-HMAC-SHA256 con 600.000 iteraciones, calculado en código nativo (con las instrucciones SHA-256 de ARMv8 si el procesador las tiene), y después AES-256-GCM con un nonce nuevo de 12 bytes y una sal nueva de 32 bytes en cada cifrado.
- Capa del dispositivo. Una clave AES-GCM por cartera, generada dentro del Keystore de Android, que nunca se puede exportar, en StrongBox si el móvil lo tiene. La bóveda exige un bloqueo de pantalla.
- Archivos. En el almacenamiento privado de la app, escritos de forma atómica y excluidos de las copias de seguridad en la nube y de la transferencia entre dispositivos.
- Intentos de contraseña. Se cuentan por cartera antes de la derivación lenta, como en iPhone; un contador dañado rechaza todos los intentos.
- Autorización. Cada firma necesita la contraseña de la cartera. El bloqueo de la app (opcional, huella o rostro) nunca autoriza una. Ninguna ventana aparece en capturas, grabaciones de pantalla ni en Recientes, las superposiciones de otras apps se ocultan en Android 12 y posteriores, y el botón Firmar y la confirmación del riesgo rechazan los toques mientras otra ventana los cubre.
- Memoria. Los secretos se guardan en contenedores que se cierran y se borran tras su uso, también cuando se cancela una operación, y cada operación que toca un secreto se ejecuta en un entorno QuickJS nuevo. La frase de recuperación se escribe en el teclado propio de la app, que ningún método de entrada ve. La JVM no puede garantizar que se borre cada copia, así que aquí tampoco la app afirma un borrado completo.
La revisión de Opus 5.5 incluye dos hallazgos de gravedad baja sobre esta capa: la clave que autentica los datos públicos de las carteras puede regenerarse tras un error del Keystore en Android 8–11 (BTS-13), y dos claves auxiliares no comprueban si hay bloqueo de pantalla (BTS-14). Consulta Revisiones de seguridad.