Modelo de amenazas
El documento completo: qué se protege, de quién, con qué medios y dónde termina la protección. Cuando algo no está verificado o no está garantizado, lo decimos.
Documento de referencia para la versión 0.1.0 (preversión). No ha sido verificado por terceros.
Para informar de un problema de seguridad, escribe a security@shelleasy.app.
Este documento dice de quién protege ShellEasy tus datos, con qué medios y dónde termina la protección. Cada afirmación se ha comprobado en el código; donde algo no está verificado o no está garantizado, se dice.
Última comprobación: versión 0.1.0 (prelanzamiento). Si cambian la criptografía, los límites de confianza o el formato de los archivos, este documento debe actualizarse en la misma modificación.
1. Qué se protege
| Bien | Dónde está | Cuánto vale |
|---|---|---|
| Claves privadas SSH, frases de contraseña, contraseñas de los servidores | columnas identities.* de la base de datos | El más alto: dan acceso a los servidores |
| Lista de hosts, nombres de usuario, carpetas, etiquetas, snippets, reglas de los túneles | base de datos | Alto: revela la infraestructura y los hábitos |
| Contraseña maestra | solo en la cabeza del usuario (opcionalmente en el llavero, tras la biometría) | Es la única llave de todo |
Claves de los servidores ya aceptadas (known_hosts) | base de datos | Impiden el ataque man-in-the-middle |
Archivos de exportación .shelleasy | donde el usuario los guarde | Contienen todo, secretos incluidos |
| Contenido de las sesiones y de los archivos transferidos | memoria, red, disco de destino | Fuera del perímetro del almacén (véase §7) |
2. Quién es el adversario
Considerado:
- Quien obtiene los archivos de la app sin la contraseña maestra — equipo robado o perdido, copia de seguridad copiada (Time Machine, nube, disco externo), otro usuario del mismo equipo, malware que lee archivos pero aún no tiene la contraseña. Es el escenario principal para el que existe el almacén.
- Quien puede leer y modificar los archivos mientras la app está cerrada — misma categoría, pero activa: puede alterar o sustituir archivos en la carpeta de datos.
- Un servidor SSH/SFTP hostil o comprometido al que se conecta el usuario: puede enviar nombres de archivo y de carpeta maliciosos, salida de terminal con secuencias especiales, claves cambiadas.
- Un archivo
.shelleasyo un~/.ssh/configconstruido a propósito y que se hace importar al usuario. - Otra máquina de la red que intenta usar un túnel o un proxy SOCKS que se haya dejado escuchando.
- Quien mira por encima del hombro o toma un equipo desbloqueado y desatendido durante unos minutos — mitigado solo por el bloqueo automático (§5.4).
No considerado (fuera del perímetro):
- Un atacante con el equipo desbloqueado y la app abierta, o con código en ejecución como el mismo usuario mientras el almacén está desbloqueado: puede leer la memoria del proceso, registrar el teclado, pedirle a la app que exporte. Ningún almacén local resiste esto.
- Un sistema operativo o un firmware comprometidos.
- El análisis de canales laterales (tiempos, consumo, caché) en un equipo compartido con quien pueda observarlos.
- Un atacante que adivina una contraseña maestra débil con un ataque sin conexión costoso pero no imposible (§5.1): la fuerza de la protección depende de la contraseña elegida.
- La seguridad de los servidores a los que uno se conecta y de la biblioteca SSH de Go (
golang.org/x/crypto/ssh), en las que se confía.
3. Cómo funciona el cifrado (en breve)
contraseña maestra --Argon2id(salt, t=3, 64 MiB, p=4)--> KEK
KEK --AES-256-GCM, AAD "easyshell/vault/dek/v1"--> DEK envuelta (en vault.json, FUERA de la base de datos)
DEK --HKDF-SHA256, info "easyshell/db/v1"--> clave de la base de datos
clave de la base de datos --Adiantum, bloques de 4 KiB--> archivo SQLite completo
DEK --AES-256-GCM, AAD "tabella|colonna|id"--> secretos individuales (segundo nivel, dentro de la base de datos)
vault.jsoncontiene solo la versión, el salt, los parámetros de Argon2id y la DEK envuelta. Está fuera de la base de datos porque hace falta para abrirla. Se escribe de forma atómica, con permisos 0600.- Base de datos completa cifrada con Adiantum (biblioteca
ncruces/go-sqlite3): nombres de host, nombres de usuario, carpetas, etiquetas, estructura de las tablas, índices, archivos de diario (journal) de SQLite. Los archivos temporales usan claves aleatorias ytemp_store=memoryevita escribirlos. - Segundo nivel para los secretos: cada clave privada, frase de contraseña y contraseña se cifra con AES-256-GCM y se vincula a su propio registro mediante el AAD
tabella|colonna|id. Un blob copiado a otro registro no se abre (verificado en las pruebas). Todo punto del código que cifra o descifra un secreto pasa porvault.FieldAADcon elidde la fila (comprobado:data.go,sessions.go,sync.go). - El frontend nunca recibe los secretos. Las identidades llegan a la interfaz solo con indicadores (
hasKey,hasPassphrase,hasPassword); la clave privada se indica con una ruta elegida en el diálogo nativo y no pasa por la webview. Comprobado eninternal/core/data.goy en los bindings deapp.go. - Archivo
.shelleasy: cabecera (magic, versión, parámetros de Argon2id, salt, nonce) autenticada como AAD, luego AES-256-GCM sobre el JSON comprimido. El dato en claro tras la descompresión está limitado a 64 MiB y el archivo leído a 128 MiB.
4. Qué queda en claro (metadatos)
Quien tiene los archivos pero no la contraseña no lee los datos, pero puede ver:
- Que existe una carpeta de datos de ShellEasy, el nombre de los archivos (
vault.json, la base de datos,backups/) y su tamaño y fechas de modificación. El tamaño de la base de datos da una idea aproximada de cuántos datos contiene; las fechas dicen cuándo se ha usado la app y cuándo se han guardado cambios. - Los parámetros del almacén: salt, costes de Argon2id, versión del formato (
vault.json). No son secretos, pero indican cuánto cuesta un intento de adivinar la contraseña. - La longitud de los secretos de forma aproximada dentro de la base de datos queda oculta (cifrados en bloques de 4 KiB), pero el tamaño total no.
- De los archivos
.shelleasy: el tamaño (≈ cantidad de datos tras la compresión) y los parámetros de la cabecera. - Fuera del almacén, según el sistema: las últimas carpetas usadas, los elementos recientes del sistema operativo, el historial de la shell local, los archivos temporales de «Edición remota» mientras están abiertos (carpeta temporal del usuario 0700, archivos 0600, eliminados al cerrar).
- Las preferencias de la interfaz guardadas en
localStoragede la webview (claveses.*): no contienen datos de los servidores.
El llavero del sistema, si se activa el desbloqueo con huella (Touch ID), contiene la contraseña maestra (§5.5).
5. Límites y decisiones, uno por uno
5.1 La contraseña maestra es la única barrera
El atacante del §2 (1) puede probar contraseñas sin conexión. Cada intento cuesta un Argon2id con 64 MiB, 3 pasadas, 4 hilos: el ataque resulta lento, pero una contraseña débil sigue siendo débil. La longitud mínima exigida es de 8 caracteres; una frase de 4-5 palabras es mucho mejor que una palabra con símbolos. No hay recuperación: si la contraseña se pierde, los datos se pierden (decisión de diseño, «zero-knowledge»). Por eso se aconseja una exportación .shelleasy.
Los parámetros no pueden bajar del mínimo (3 pasadas, 64 MiB, 4 hilos) ni subir por encima de un límite máximo (16 pasadas, 1 GiB, 16 hilos): véase §6.1.
5.2 Cifrado de la base de datos: qué da Adiantum y qué no
Adiantum es un cifrado de disco (el archivo se divide en bloques de 4 KiB, cada uno cifrado por entero, de modo que el mismo texto en claro en la misma posición da el mismo texto cifrado). Su modelo de amenaza es: el adversario ve varias copias del disco y aprende únicamente qué bloques han cambiado. De ahí se siguen tres límites, que también valen para ShellEasy:
- Es determinista. Quien tiene dos copias de la base de datos (por ejemplo, dos copias de seguridad de días distintos) ve qué bloques de 4 KiB han cambiado, cuáles no y cuáles han vuelto a un valor anterior, pero no qué contienen. Un
VACUUMreconstruye el archivo y borra buena parte de esta información; ShellEasy no lo ejecuta por iniciativa propia. - No autentica las páginas. Un bloque modificado se descifra en datos aleatorios (el bloque entero, no un solo bit), algo que SQLite normalmente detecta como corrupción, pero no es una garantía: Adiantum no declara protección frente a manipulaciones. Un atacante que modifica el archivo puede, por tanto, en el peor de los casos, corromper o hacer perder datos, o volver a presentar bloques o archivos completos genuinos que ya existieron (véase el punto 3); sin la clave no puede construir contenidos elegidos por él.
- No protege frente a la restauración de una copia antigua. Quien sustituye la base de datos por una versión anterior suya (misma clave) obtiene una base de datos válida pero antigua: los hosts e identidades borrados o modificados después de esa fecha reaparecen como estaban. La app no lo advierte.
Qué protege de todos modos la integridad de los secretos. Las credenciales no dependen de Adiantum para su autenticidad: son blobs AES-GCM con AAD vinculado al registro. Un bloque manipulado o un blob desplazado hace fallar la apertura del secreto (error, no dato erróneo). Un blob antiguo pero genuino del mismo registro (restauración de un único secreto o de todo el archivo) sí se abre: es el límite del punto 3.
Consecuencias prácticas para el usuario:
- Una copia de la base de datos no es una copia de seguridad segura para guardar donde otros pueden escribirla: una copia de seguridad sirve para recuperar, no para fiarse. Las copias de seguridad de la app (
backups/, las últimas 10 copias hechas antes de cada importación) son copias cifradas y con permisos 0700/0600. - La base de datos nunca se reconstruye ni se sustituye automáticamente a partir de una copia no verificada.
- El soporte de recuperación más sólido es la exportación
.shelleasy(AES-GCM, autenticada por completo).
5.3 vault.json: MAC y contador de versión — evaluados y no adoptados
Se evaluó un control de integridad del archivo, por ejemplo un MAC sobre vault.json más un contador de versión. Conclusión:
- Un MAC sobre
vault.jsonno aporta nada. El archivo ya está autenticado en lo que importa: la DEK envuelta es un blob AES-GCM con AAD; cualquier modificación del salt, de los parámetros o del blob cambia la clave derivada o rompe la autenticación, y el desbloqueo falla (ErrWrongPassword), sin producir nunca una clave errónea. El atacante del §2 (2) puede impedir el desbloqueo (borrar o corromper el archivo), no secuestrarlo. - Un contador de versión no se sostiene sin un punto fijo fuera del alcance del adversario. Si el contador está en la misma carpeta, quien puede restaurar un archivo antiguo restaura también el contador. Haría falta un ancla en el llavero del sistema (que existe solo donde el llavero está disponible y vinculado a la biometría, hoy solo macOS) y seguiría siendo válido solo frente a quien no pueda modificar también eso. No se adopta ahora; queda como idea registrada en el issue de seguimiento (§9).
- La restauración de un
vault.jsonantiguo tiene, sin embargo, un efecto real: la DEK no cambia cuando se cambia la contraseña maestra (solo se vuelve a envolver, decisión de diseño para no recifrar todo). Por tanto, quien conoce la antigua contraseña y tiene una copia antigua devault.json(por ejemplo en una copia de seguridad) y una copia de la base de datos todavía puede abrirla, incluso después de que el usuario haya cambiado la contraseña por sospechar que estaba comprometida. Si piensas que la contraseña ha sido comprometida, cambiarla no basta: crea un almacén nuevo con una contraseña nueva e importa una exportación.shelleasy. Una rotación de la DEK al cambiar la contraseña queda registrada en el seguimiento (§9).
5.4 Bloqueo, sesiones abiertas y memoria
- Bloqueo automático tras 15 minutos de inactividad de la interfaz (valor fijo, todavía no configurable) y bajo petición. El bloqueo cierra la base de datos y pone a cero la DEK. Las sesiones SSH ya autenticadas y los túneles siguen activos tras el bloqueo (decisión D3, para no interrumpir un trabajo en curso); para abrir otras nuevas hay que desbloquear. Quien quiera cerrar todo debe cerrar las sesiones o la app.
- Puesta a cero no garantizada. La app mantiene los secretos como
[]bytey los pone a cero tras el uso (vault.Zero; el bloqueo pone a cero la DEK), pero Go no asegura que el recolector de basura o el crecimiento de la pila no dejen copias, y no todos los datos pasan por[]byte:- la contraseña maestra llega desde la interfaz como cadena (el puente entre la webview y Go la transporta así) y quedan copias en la memoria de la webview y de los búferes del puente que la app no controla;
- la clave de la base de datos pasa al controlador de SQLite como cadena hexadecimal (
PRAGMA hexkey, inmutable en Go) y vive en la memoria del controlador hasta que el proceso termina; cerrar la base de datos no es un borrado verificado; - las claves privadas leídas para autenticar una sesión permanecen en la memoria del cliente SSH mientras la sesión existe;
- la memoria no está bloqueada (
mlock) y puede acabar en el swap o en un archivo de volcado/hibernación. Por tanto, quien pueda leer la memoria del proceso o un volcado del disco/RAM puede encontrar secretos incluso después del bloqueo. Es un límite declarado, no un defecto aislado: véase también §2.
- En los registros y en los mensajes de error no aparecen secretos (comprobado mediante búsqueda en el código: no hay ningún registro de aplicación; el único
log.Fatalfestá enmain.goe imprime el error de arranque de Wails).
5.5 Desbloqueo con huella (Touch ID)
La contraseña maestra la custodia el sistema tras la biometría y la app nunca la escribe en disco. En macOS, con la app firmada y un perfil de aprovisionamiento, el secreto está en el llavero con kSecAccessControlBiometryCurrentSet y WhenUnlockedThisDeviceOnly: el sistema pide la huella al leer y el secreto se invalida si cambian las huellas registradas. Una compilación sin firmar o con firma ad hoc usa un nivel más débil (app): el secreto está igualmente en el llavero y otro programa no lo lee sin el consentimiento del sistema, pero la petición de la huella es un control de la app y no del sistema; la interfaz indica el nivel en uso. El desbloqueo usa el mismo Unlock: no existe un camino alternativo hacia la clave. No verificado en Windows (Windows Hello) ni en Linux: todavía no están implementados.
5.6 Claves de los servidores (TOFU)
En la primera conexión la app muestra la huella SHA256 y pide confirmación; la clave se guarda en el dispositivo. Si después el servidor presenta una clave diferente (o de otro tipo respecto a las conocidas) la conexión se bloquea con un error explícito, sin posibilidad de aceptar sobre la marcha (HostKeyChangedError). No existe ningún InsecureIgnoreHostKey en el código (comprobado). Límite intrínseco de TOFU: si la primera conexión ya está interceptada, el ataque no se ve; la verificación de la huella por un canal distinto queda a cargo del usuario. Las claves conocidas no se sincronizan entre dispositivos: la confianza es por dispositivo.
5.7 Un servidor SFTP hostil
El servidor elige los nombres de los archivos que lista. Mientras los nombres se usen solo para mostrarlos, el riesgo es bajo (la interfaz no usa v-html/innerHTML: comprobado). El riesgo real está en las transferencias: al descargar una carpeta, un nombre como ..\..\Startup\x.exe en Windows, donde \ separa las carpetas, salía de la carpeta elegida por el usuario. Corregido con esta revisión (§6.2): los nombres leídos de un lado remoto se comprueban antes de construir cualquier ruta local; los peligrosos se omiten y aparecen en las notas de la transferencia. El cliente SFTP ya descarta . y .. y usa path.Base para los nombres. Los enlaces simbólicos remotos nunca se siguen en las transferencias.
Además, los archivos entrantes ahora nacen con permisos 0600 y toman los permisos del original solo cuando la copia ha terminado, de modo que un archivo reservado no es legible por otros usuarios mientras está incompleto.
5.8 Un archivo .shelleasy hostil
La cabecera está autenticada, pero sus partes numéricas se leen antes de verificar la contraseña. Un archivo construido para hacer asignar mucha memoria o calcular durante horas debía rechazarse de antemano: corregido con el límite máximo de los parámetros (§6.1). Tras la apertura: tamaño máximo del archivo leído (128 MiB), del sobre descomprimido (64 MiB, frente a las «bombas» de descompresión), número máximo de elementos (200.000), validación de cada campo, ningún contenido ejecutado. La importación muestra una vista previa, hace una copia de seguridad de la base de datos y es una única transacción. Un archivo hostil proporcionado por quien conoce la contraseña puede, de todos modos, insertar hosts o snippets con contenido elegido por él: los snippets rechazan los caracteres de control (ESC, C1, DEL), que en el terminal servirían para ocultar o alterar lo que se pega; el usuario ve en cualquier caso el comando final antes de insertarlo, si hay parámetros.
5.9 Importación desde ~/.ssh/config
El archivo se lee y se interpreta (incluidos Include anidados con un límite de profundidad y detección de ciclos) pero no se ejecuta nada: ProxyCommand no es compatible, el host se señala como no importable y la app nunca lanza ese comando.
5.10 Túneles y proxy
Por defecto se escucha solo en 127.0.0.1. Si se elige una dirección accesible desde la red, la app muestra un aviso en el diálogo y en la lista (con SOCKS5 sería un proxy abierto a todos, sin autenticación). Un túnel local escuchando en 127.0.0.1 es, de todos modos, accesible para cualquier programa del mismo equipo (otros usuarios incluidos, en sistemas multiusuario): es el comportamiento de ssh -L y se documenta, no está mitigado. Bloquear el almacén no detiene los túneles.
5.11 La webview y el frontend
El frontend es una página local servida desde el paquete de la app, sin contenido remoto, sin v-html/innerHTML/eval (comprobado), con las herramientas de desarrollo de Nuxt desactivadas (y el inspector de la webview, que Wails habilita solo en las compilaciones de depuración). La salida de los terminales pasa por xterm.js como texto. Falta una Content-Security-Policy: hoy no sería trivial establecerla porque la página generada contiene scripts en línea (mapa de importaciones y datos iniciales); hacerla estricta requiere hashes o nonces. Está registrada como mejora (§9): sería una defensa en profundidad, no la corrección de un fallo conocido.
5.12 Actualizaciones y cadena de suministro
Ninguna actualización automática y ninguna telemetría: el frontend no carga recursos remotos y las únicas conexiones salientes son las que van hacia los hosts SSH que el usuario abre y, solo si lo activa, hacia el servidor del asistente de IA (§5.13). El único cliente HTTP de la app es internal/ai. Las dependencias de Go y npm están fijadas por los archivos de bloqueo; appimagetool está fijado por versión y checksum. Las compilaciones de lanzamiento todavía no están firmadas (macOS/Windows): mientras no lo estén, la integridad del paquete descargado solo se verifica con SHA256SUMS. Todavía no hay un análisis automático de dependencias vulnerables: queda por añadir (§9).
5.13 Asistente de IA local
El asistente (explica, de descripción a comando, resume, ayuda con snippets) está desactivado por defecto y abre un canal nuevo: lleva texto del terminal a un servidor de modelos elegido por el usuario. No se incluye ningún modelo y no sale ninguna solicitud sin que el usuario la haya visto y enviado.
Adónde puede ir el texto.
- Por defecto solo al equipo del usuario: son «locales» únicamente
localhosty las direcciones de loopback escritas por extenso (127.x,::1). Cualquier nombre se considera remoto aunque hoy apunte al equipo local, porque la resolución de nombres puede cambiar (DNS rebinding). - Un servidor no local solo se puede usar tras una confirmación explícita, vinculada a ese nombre y ese puerto: cambiar de servidor anula la confirmación. Con un servidor no local y una clave se exige
https://. - En el momento de la conexión, para los servidores locales, la dirección efectiva debe ser de loopback; de lo contrario, la conexión se rechaza. Las redirecciones no se siguen y las variables de entorno de los proxies se ignoran.
- La clave del servidor, si la hay, está cifrada en la base de datos (AES-GCM con AAD dedicado), nunca sale hacia la interfaz y no se envía a un servidor distinto del configurado. La configuración no se sincroniza.
Qué sale. Cada solicitud pasa por una vista previa construida por el backend: el texto mostrado es el que sale, byte a byte (probado), hacia el destino mostrado (host, puerto, modelo); si los ajustes cambian después de la vista previa, el envío se rechaza. Antes de la vista previa, el texto y la solicitud pasan por un filtro que oculta claves privadas, credenciales en las direcciones, cabeceras de autorización, tokens conocidos (GitHub, AWS, Slack, JWT…), valores de variables con nombres como password, secret, token, api_key, opciones de línea de comandos (--password, sshpass -p, mysql -p…, curl -u) y las contraseñas y frases de contraseña guardadas en el almacén si aparecen en el texto. La vista previa indica qué se ha ocultado. Del terminal sale como máximo la cola de 12.000 caracteres.
Límites del filtro (declarados también en la vista previa). Reconoce los secretos por su forma y por valor conocido: no puede reconocer una contraseña escrita en claro sin un nombre que la delate, un secreto que no está en el almacén, ni información sensible que no son secretos (nombres de host, direcciones IP, rutas, contenido de los archivos, datos personales en la salida). Con un servidor no local todo esto sale del equipo. Por eso hay que leer la vista previa.
Programas que la app inicia (detección de servidores). El botón Iniciar hace que la app ejecute un programa externo, por lo que es un punto que hay que mantener estrecho: solo se ejecutan ollama serve y lms server start|stop, buscados con nombre fijo en el PATH y en rutas conocidas; ningún comando, argumento o ruta proviene del usuario, de la interfaz o del modelo. Ollama se inicia con OLLAMA_HOST=127.0.0.1:porta impuesto, de modo que no pueda ponerse a escuchar en la red; los procesos iniciados por la app se cierran con la app y Detener afecta solo a esos. Riesgo residual: quien ya puede colocar un ejecutable llamado ollama en el PATH del usuario obtiene la ejecución cuando el usuario pulsa Iniciar (mismo nivel que quien ya puede modificar el entorno del usuario, §2). El botón Descargar abre en el navegador una página elegida de una tabla fija en el backend (HTTPS, tres direcciones oficiales): la app no descarga ni instala nada y no hace conexiones hacia Internet; lo que se descarga y se instala lo decide el usuario en el navegador. Los comandos de lms todavía no se han probado en un LM Studio real.
El modelo es una entidad no fiable.
- Salida de terminal hostil (prompt injection): un servidor o un archivo puede hacer imprimir texto que «da órdenes» al modelo. Mitigaciones: el texto va encerrado entre delimitadores que no puede cerrar (están neutralizados) y las instrucciones dicen que se trate como dato; el modelo no tiene herramientas ni acceso al sistema, por lo que solo puede producir texto. Sigue siendo posible que la explicación sea engañosa o que el comando propuesto sea dañino.
- Comando propuesto: nunca se ejecuta. Se inserta en el prompt como texto pegado (sin Enter) y lo lanza el usuario; la interfaz muestra el comando en un recuadro, avisa si tiene varias líneas (la shell podría no admitir el pegado protegido) y le quita todo carácter de control, ESC incluido, para que una respuesta hostil no pueda cerrar el pegado protegido y hacer ejecutar otra cosa. El riesgo residual es que el usuario pulse Enter sin leer.
- Respuesta excesiva o lenta: límite de 256 KB por respuesta, interrupción tras 120 s sin datos, como máximo 3 solicitudes a la vez.
Bloqueo del almacén. Las solicitudes en curso se interrumpen y las vistas previas aún no enviadas se olvidan.
No verificado: la compatibilidad con los servidores reales (Ollama, LM Studio, llama.cpp) se ha probado con un servidor simulado que imita la API de OpenAI, no con los programas reales; los formatos del streaming pueden tener variantes no previstas. El filtro no se ha evaluado con salidas reales de gran tamaño: los falsos positivos (texto ocultado sin motivo) son posibles y preferibles a los falsos negativos.
6. Correcciones hechas con esta revisión
| # | Problema | Corrección | Prueba |
|---|---|---|---|
| 6.1 | Los parámetros Argon2id de vault.json y de los sobres .shelleasy tenían solo un mínimo: una cabecera hostil podía pedir gigabytes u horas de cálculo antes de la comprobación de la contraseña | Límite máximo: 16 pasadas, 1 GiB, 16 hilos (vault.MaxTime/MaxMemoryKiB/MaxThreads, error ErrHugeParams), aplicado en creación, desbloqueo, exportación e importación | TestParamsCeiling, TestHugeParamsRejectedOnOpen: el rechazo se produce en menos de un segundo, sin derivar claves |
| 6.2 | Nombres de archivo leídos de un servidor SFTP usados sin control para construir rutas locales (salida de la carpeta de destino en Windows; nombres reservados; flujos alternativos NTFS) | unsafeName / safeReadDir / checkTopName: descartados "", ., .., /, NUL y, en Windows, \ : * ? " < > |, caracteres de control, nombres que terminan en punto/espacio y nombres de dispositivo (CON, NUL, COM1…); los descartados van a las notas de la transferencia | TestUnsafeName, TestSafeReadDirSkipsHostileNames |
| 6.3 | Archivos locales creados con permisos 0644 durante la descarga | Creación con 0600; los permisos del original se aplican cuando la copia ha terminado | TestLocalCreateIsPrivate |
La suite completa de Go pasa también con -race. No verificado: el comportamiento de 6.2 en un Windows real (las reglas de Windows se prueban con una tabla, no sobre un sistema de archivos Windows).
7. Fuera del almacén: lo que la app no protege
- Los datos que pasan por las sesiones (salida del terminal, archivos transferidos, lo que se pega): no están cifrados en reposo y dependen del equipo y del servidor. Si se usa el asistente de IA, el texto que se decide enviar sale del almacén hacia el servidor de modelos (§5.13). El búfer de desplazamiento del terminal y la shell local usan la memoria y los archivos del sistema operativo.
- Los archivos descargados permanecen en el disco de destino con las protecciones de ese disco.
- El portapapeles del sistema: un secreto copiado permanece en el portapapeles hasta que algo lo sustituya.
- Las exportaciones
.shelleasyestán protegidas por su contraseña durante toda su vida: conservarlas como se conservarían las llaves.
8. Revisión externa
Recomendación: sí, antes de la versión 1.0 y antes de recomendar la app para credenciales de producción, limitada a un perímetro pequeño y claro:
internal/vault,internal/envelope,internal/storey el punto de desbloqueo (internal/core/core.go): unas 1.000 líneas, una revisión de pocos días.- La gestión de las claves de los servidores (
internal/sshconn/hostkey.go) y de las transferencias (§5.7). - La integración biométrica en macOS (
internal/biometric).
No hay presupuesto previsto: alternativas sin coste son una revisión entre pares por parte de quien conozca Go y criptografía, la publicación del código con este documento para atraer avisos, y la activación de las alertas de seguridad de GitHub. Mientras no se haga una revisión, la fórmula honesta es: criptografía con primitivas estándar y decisiones documentadas, no verificada por terceros.
9. Seguimientos abiertos
- Rotación de la DEK al cambiar la contraseña (opcional): recifrar la base de datos y los secretos con una DEK nueva, de modo que una copia antigua de
vault.jsonmás la contraseña antigua ya no abran nada. Es una operación larga y delicada (copia atómica, reversión en caso de error). - Detectar la restauración de copias antiguas con un ancla en el llavero del sistema (§5.3).
- Content-Security-Policy para la webview (§5.11).
- Análisis automático de dependencias (
govulncheck,pnpm audit) en las comprobaciones (§5.12). - Firma de los paquetes y verificación del AppImage/Windows.
- Windows Hello y desbloqueo biométrico en Linux.