Aller au contenu

← Retour au résumé

Modèle de menace

Le document complet : ce qui est protégé, contre qui, par quels moyens et où la protection s’arrête. Quand une chose n’est pas vérifiée ou pas garantie, nous le disons.

Document de référence pour la version 0.1.0 (préversion). Il n’a pas été vérifié par des tiers.

Pour signaler un problème de sécurité, écrivez à security@shelleasy.app.

Ce document dit contre qui ShellEasy protège vos données, avec quels moyens et où la protection s’arrête. Chaque affirmation a été vérifiée dans le code ; lorsqu’un point n’est pas vérifié ou n’est pas garanti, le document le dit.

Dernière vérification : version 0.1.0 (pré-version). Si le chiffrement, les frontières de confiance ou le format des fichiers changent, ce document doit être mis à jour dans la même modification.

1. Ce qui est protégé

BienOù il se trouveSa valeur
Clés privées SSH, phrases secrètes, mots de passe des serveurscolonnes identities.* de la base de donnéesLa plus élevée : elles donnent accès aux serveurs
Liste des hôtes, noms d’utilisateur, dossiers, tags, snippets, règles des tunnelsbase de donnéesÉlevée : elle révèle l’infrastructure et les habitudes
Mot de passe maîtreuniquement dans la tête de l’utilisateur (éventuellement dans le trousseau, derrière la biométrie)C’est la seule clé de l’ensemble
Clés des serveurs déjà acceptées (known_hosts)base de donnéesElles empêchent l’attaque de l’homme du milieu
Fichiers d’export .shelleasylà où l’utilisateur les enregistreIls contiennent tout, secrets compris
Contenu des sessions et des fichiers transférésmémoire, réseau, disque de destinationHors du périmètre du coffre (voir §7)

2. Qui est l’adversaire

Pris en compte :

  1. Qui obtient les fichiers de l’app sans le mot de passe maître — ordinateur volé ou perdu, sauvegarde copiée (Time Machine, cloud, disque externe), autre utilisateur du même ordinateur, logiciel malveillant qui lit les fichiers mais ne connaît pas encore le mot de passe. C’est le scénario principal pour lequel le coffre existe.
  2. Qui peut lire et modifier les fichiers pendant que l’app est fermée — même catégorie, mais active : cette personne peut altérer ou remplacer des fichiers dans le dossier des données.
  3. Un serveur SSH/SFTP hostile ou compromis auquel l’utilisateur se connecte : il peut envoyer des noms de fichiers et de dossiers malveillants, une sortie de terminal avec des séquences spéciales, des clés modifiées.
  4. Un fichier .shelleasy ou un ~/.ssh/config construit pour nuire et que l’utilisateur est amené à importer.
  5. Une autre machine du réseau qui essaie d’utiliser un tunnel ou un proxy SOCKS laissé à l’écoute.
  6. Qui regarde par-dessus l’épaule ou prend un ordinateur déverrouillé et laissé sans surveillance pendant quelques minutes — atténué uniquement par le verrouillage automatique (§5.4).

Non pris en compte (hors du périmètre) :

  • Un attaquant disposant de l’ordinateur déverrouillé et de l’app ouverte, ou de code exécuté avec le même utilisateur pendant que le coffre est déverrouillé : il peut lire la mémoire du processus, enregistrer le clavier, demander à l’app d’exporter. Aucun coffre local n’y résiste.
  • Un système d’exploitation ou un firmware compromis.
  • L’analyse des canaux auxiliaires (temps, consommation, caches) sur un ordinateur partagé avec une personne qui peut les observer.
  • Un attaquant qui devine un mot de passe maître faible par une attaque hors ligne coûteuse mais pas impossible (§5.1) : la solidité de la protection dépend du mot de passe choisi.
  • La sécurité des serveurs auxquels on se connecte et de la bibliothèque SSH de Go (golang.org/x/crypto/ssh), auxquels on fait confiance.

3. Comment fonctionne le chiffrement (en bref)

Mot de passe maître --Argon2id(salt, t=3, 64 MiB, p=4)--> KEK
KEK --AES-256-GCM, AAD "easyshell/vault/dek/v1"--> DEK enveloppée   (dans vault.json, HORS de la base de données)
DEK --HKDF-SHA256, info "easyshell/db/v1"--> clé de la base de données
clé de la base de données --Adiantum, blocs de 4 KiB--> fichier SQLite entier
DEK --AES-256-GCM, AAD "table|colonne|id"--> secrets individuels (second niveau, dans la base de données)
  • vault.json contient uniquement la version, le sel, les paramètres d’Argon2id et la DEK enveloppée. Il se trouve hors de la base de données parce qu’il sert à l’ouvrir. Écrit de façon atomique, permissions 0600.
  • Base de données entièrement chiffrée avec Adiantum (bibliothèque ncruces/go-sqlite3) : noms d’hôtes, noms d’utilisateur, dossiers, tags, structure des tables, index, fichiers de journal de SQLite. Les fichiers temporaires utilisent des clés aléatoires et temp_store=memory évite d’en écrire.
  • Second niveau pour les secrets : chaque clé privée, phrase secrète et mot de passe est chiffré avec AES-256-GCM et lié à son enregistrement par l’AAD tabella|colonna|id. Un blob copié dans un autre enregistrement ne s’ouvre pas (vérifié dans les tests). Chaque endroit du code qui chiffre ou déchiffre un secret passe par vault.FieldAAD avec l’id de la ligne (vérifié : data.go, sessions.go, sync.go).
  • Le frontend ne reçoit jamais les secrets. Les identités parviennent à l’interface avec de simples indicateurs (hasKey, hasPassphrase, hasPassword) ; la clé privée est désignée par un chemin choisi dans la boîte de dialogue native et ne passe pas par la webview. Vérifié dans internal/core/data.go et dans les liaisons de app.go.
  • Fichiers .shelleasy : en-tête (magic, version, paramètres d’Argon2id, sel, nonce) authentifié comme AAD, puis AES-256-GCM sur le JSON compressé. La donnée en clair après décompression est limitée à 64 MiB et le fichier lu à 128 MiB.

4. Ce qui reste en clair (métadonnées)

Qui a les fichiers mais pas le mot de passe ne lit pas les données, mais peut voir :

  • Qu’un dossier de données de ShellEasy existe, le nom des fichiers (vault.json, la base de données, backups/) et leur taille et leurs dates de modification. La taille de la base de données donne une idée approximative de la quantité de données qu’elle contient ; les dates indiquent quand l’app a été utilisée et quand des modifications ont été enregistrées.
  • Les paramètres du coffre : sel, coûts d’Argon2id, version du format (vault.json). Ce ne sont pas des secrets, mais ils indiquent ce que coûte un essai pour deviner le mot de passe.
  • La longueur approximative des secrets à l’intérieur de la base de données est masquée (chiffrés par blocs de 4 KiB), mais pas la taille globale.
  • Pour les fichiers .shelleasy : la taille (≈ quantité de données après compression) et les paramètres de l’en-tête.
  • Hors du coffre, selon le système : les derniers dossiers utilisés, les éléments récents du système d’exploitation, l’historique du shell local, les fichiers temporaires de « Modification à distance » tant qu’ils sont ouverts (dossier temporaire de l’utilisateur 0700, fichiers 0600, supprimés à la fermeture).
  • Les préférences de l’interface enregistrées dans le localStorage de la webview (clés es.*) : elles ne contiennent pas de données sur les serveurs.

Le trousseau du système, si le déverrouillage par empreinte (Touch ID) est activé, contient le mot de passe maître (§5.5).

5. Limites et choix, un par un

5.1 Le mot de passe maître est la seule barrière

L’attaquant du §2 (1) peut essayer des mots de passe hors ligne. Chaque essai coûte un Argon2id avec 64 MiB, 3 passes, 4 threads : l’attaque est ralentie, mais un mot de passe faible reste faible. La longueur minimale exigée est de 8 caractères ; une phrase de 4 ou 5 mots vaut bien mieux qu’un mot avec des symboles. Il n’existe pas de récupération : si le mot de passe est perdu, les données sont perdues (choix de conception, « zero-knowledge »). C’est pourquoi un export .shelleasy est conseillé.

Les paramètres ne peuvent pas descendre sous le minimum (3 passes, 64 MiB, 4 threads) ni dépasser un plafond (16 passes, 1 GiB, 16 threads) : voir §6.1.

5.2 Chiffrement de la base de données : ce qu’Adiantum apporte et ce qu’il n’apporte pas

Adiantum est un chiffrement de disque (le fichier est divisé en blocs de 4 KiB, chacun chiffré en entier, un même texte en clair au même endroit donnant le même texte chiffré). Son modèle de menace est le suivant : l’adversaire voit plusieurs copies du disque et apprend uniquement quels blocs ont changé. Il en découle trois limites, qui valent aussi pour ShellEasy :

  1. Il est déterministe. Qui a deux copies de la base de données (par exemple deux sauvegardes de jours différents) voit quels blocs de 4 KiB ont changé, lesquels non et lesquels sont revenus à une valeur précédente, mais pas ce qu’ils contiennent. Un VACUUM reconstruit le fichier et efface une grande partie de cette information ; ShellEasy ne l’exécute pas de sa propre initiative.
  2. Il n’authentifie pas les pages. Un bloc modifié se déchiffre en données aléatoires (le bloc entier, pas un seul bit), ce que SQLite détecte normalement comme une corruption, mais ce n’est pas une garantie : Adiantum ne revendique pas de protection contre la falsification. Un attaquant qui modifie le fichier peut donc, dans le pire des cas, corrompre ou faire perdre des données, ou bien rejouer des blocs ou des fichiers entiers authentiques ayant déjà existé (voir le point 3) ; sans la clé, il ne peut pas construire des contenus de son choix.
  3. Il ne protège pas contre la restauration d’une ancienne copie. Qui remplace la base de données par une de ses versions antérieures (même clé) obtient une base de données valide mais ancienne : les hôtes et les identités supprimés ou modifiés après cette date réapparaissent tels qu’ils étaient. L’app ne s’en aperçoit pas.

Ce qui protège malgré tout l’intégrité des secrets. L’authenticité des identifiants ne dépend pas d’Adiantum : ce sont des blobs AES-GCM avec une AAD liée à l’enregistrement. Un bloc altéré ou un blob déplacé fait échouer l’ouverture du secret (une erreur, pas une donnée erronée). En revanche, un blob ancien mais authentique du même enregistrement (retour en arrière d’un seul secret ou du fichier entier) s’ouvre : c’est la limite du point 3.

Conséquences pratiques pour l’utilisateur :

  • Une copie de la base de données n’est pas une sauvegarde sûre à garder là où d’autres peuvent écrire : une sauvegarde sert à récupérer, pas à faire confiance. Les sauvegardes de l’app (backups/, les 10 dernières copies faites avant chaque importation) sont des copies chiffrées, avec des permissions 0700/0600.
  • La base de données n’est jamais reconstruite ni remplacée automatiquement à partir d’une copie non vérifiée.
  • Le support de récupération le plus solide est l’export .shelleasy (AES-GCM, authentifié en entier).

5.3 vault.json : MAC et compteur de version — évalués et non adoptés

Un contrôle d’intégrité du fichier a été évalué, par exemple un MAC sur vault.json plus un compteur de version. Conclusion :

  • Un MAC sur vault.json n’apporte rien. Le fichier est déjà authentifié pour ce qui compte : la DEK enveloppée est un blob AES-GCM avec AAD ; toute modification du sel, des paramètres ou du blob change la clé dérivée ou rompt l’authentification, et le déverrouillage échoue (ErrWrongPassword), sans jamais produire une mauvaise clé. L’attaquant du §2 (2) peut empêcher le déverrouillage (supprimer ou corrompre le fichier), pas le détourner.
  • Un compteur de version ne tient pas sans un point fixe hors de portée de l’adversaire. Si le compteur se trouve dans le même dossier, qui peut restaurer un ancien fichier restaure aussi le compteur. Il faudrait une ancre dans le trousseau du système (qui n’existe que là où le trousseau est disponible et lié à la biométrie, aujourd’hui uniquement macOS) et elle ne resterait valable que pour qui ne peut pas modifier aussi celle-ci. Il n’est pas adopté pour l’instant ; l’idée reste consignée dans l’issue de suivi (§9).
  • La restauration d’un ancien vault.json a cependant un effet réel : la DEK ne change pas quand on change le mot de passe maître (elle est seulement réenveloppée, choix de conception pour ne pas tout rechiffrer). Par conséquent, qui connaît l’ancien mot de passe et possède une ancienne copie de vault.json (par exemple dans une sauvegarde) et une copie de la base de données peut encore l’ouvrir, même après que l’utilisateur a changé de mot de passe parce qu’il soupçonnait qu’il était compromis. Si vous pensez que le mot de passe a été compromis, le changer ne suffit pas : créez un nouveau coffre avec un nouveau mot de passe et importez un export .shelleasy. Une rotation de la DEK lors du changement de mot de passe est consignée dans le suivi (§9).

5.4 Verrouillage, sessions ouvertes et mémoire

  • Verrouillage automatique après 15 minutes d’inactivité de l’interface (valeur fixe, pas encore configurable) et sur demande. Le verrouillage ferme la base de données et met la DEK à zéro. Les sessions SSH déjà authentifiées et les tunnels restent actifs après le verrouillage (décision D3, pour ne pas interrompre un travail en cours) ; pour en ouvrir de nouveaux, il faut déverrouiller. Pour tout fermer, il faut fermer les sessions ou l’app.
  • Mise à zéro non garantie. L’app conserve les secrets sous forme de []byte et les met à zéro après usage (vault.Zero ; le verrouillage met la DEK à zéro), mais Go ne garantit pas que le garbage collector ou la croissance de la pile ne laissent pas de copies, et toutes les données ne passent pas par des []byte :
    • le mot de passe maître arrive de l’interface sous forme de chaîne (le pont entre la webview et Go le transporte ainsi) et il en reste des copies dans la mémoire de la webview et dans des tampons du pont que l’app ne contrôle pas ;
    • la clé de la base de données est transmise au pilote SQLite sous forme de chaîne hexadécimale (PRAGMA hexkey, immuable en Go) et vit dans la mémoire du pilote jusqu’à la fin du processus ; fermer la base de données n’est pas un effacement vérifié ;
    • les clés privées lues pour authentifier une session restent dans la mémoire du client SSH tant que la session existe ;
    • la mémoire n’est pas verrouillée (mlock) et peut finir dans le swap ou dans un fichier de crash ou d’hibernation. Par conséquent, qui peut lire la mémoire du processus ou un dump du disque ou de la RAM peut trouver des secrets même après le verrouillage. C’est une limite déclarée, pas un défaut isolé : voir aussi §2.
  • Les journaux et les messages d’erreur ne contiennent pas de secrets (vérifié par recherche dans le code : aucun journal applicatif ; le seul log.Fatalf se trouve dans main.go et affiche l’erreur de démarrage de Wails).

5.5 Déverrouillage par empreinte

Le mot de passe maître est conservé par le système derrière la biométrie et n’est jamais écrit sur le disque par l’app. Sur macOS, avec une app signée et un profil de provisionnement, le secret se trouve dans le trousseau avec kSecAccessControlBiometryCurrentSet et WhenUnlockedThisDeviceOnly : le système demande l’empreinte à la lecture et le secret est invalidé si les empreintes enregistrées changent. Une build non signée ou avec une signature ad-hoc utilise un niveau plus faible (app) : le secret se trouve quand même dans le trousseau et un autre programme ne le lit pas sans l’accord du système, mais la demande d’empreinte est un contrôle de l’app et non du système ; l’interface indique le niveau utilisé. Le déverrouillage utilise le même Unlock : il n’existe pas de chemin alternatif vers la clé. Non vérifié sur Windows (Windows Hello) et Linux : ils ne sont pas encore implémentés.

5.6 Clés des serveurs (TOFU)

À la première connexion, l’app affiche l’empreinte SHA256 et demande confirmation ; la clé est enregistrée sur l’appareil. Si le serveur présente ensuite une clé différente (ou d’un autre type que celles connues), la connexion est bloquée avec une erreur explicite, sans possibilité d’accepter à la volée (HostKeyChangedError). Il n’existe aucun InsecureIgnoreHostKey dans le code (vérifié). Limite intrinsèque de TOFU : si la première connexion est déjà interceptée, l’attaque ne se voit pas ; la vérification de l’empreinte par un autre canal reste à la charge de l’utilisateur. Les clés connues ne sont pas synchronisées entre appareils : la confiance est propre à chaque appareil.

5.7 Un serveur SFTP hostile

Le serveur choisit les noms des fichiers qu’il liste. Tant que les noms servent uniquement à être affichés, le risque est faible (l’interface n’utilise pas v-html/innerHTML : vérifié). Le risque réel se trouve dans les transferts : en téléchargeant un dossier, un nom comme ..\..\Startup\x.exe sous Windows, où \ sépare les dossiers, sortait du dossier choisi par l’utilisateur. Corrigé avec cette révision (§6.2) : les noms lus d’un côté distant sont contrôlés avant de construire tout chemin local ; les noms dangereux sont ignorés et figurent dans les notes du transfert. Le client SFTP écarte déjà . et .. et utilise path.Base pour les noms. Les liens symboliques distants ne sont jamais suivis lors des transferts.

De plus, les fichiers entrants sont désormais créés avec les permissions 0600 et ne reprennent celles de l’original qu’une fois la copie terminée, de sorte qu’un fichier confidentiel ne soit pas lisible par d’autres utilisateurs tant qu’il est partiel.

5.8 Un fichier .shelleasy hostile

L’en-tête est authentifié, mais ses parties numériques sont lues avant de vérifier le mot de passe. Un fichier construit pour faire allouer beaucoup de mémoire ou calculer pendant des heures devait être rejeté d’emblée : corrigé par le plafond des paramètres (§6.1). Après l’ouverture : taille maximale du fichier lu (128 MiB), de l’enveloppe décompressée (64 MiB, contre les « bombes » de décompression), nombre maximal d’éléments (200 000), validation de chaque champ, aucun contenu exécuté. L’importation affiche un aperçu, fait une copie de sécurité de la base de données et constitue une transaction unique. Un fichier hostile fourni par quelqu’un qui connaît le mot de passe peut toutefois insérer des hôtes ou des snippets au contenu de son choix : les snippets refusent les caractères de contrôle (ESC, C1, DEL), qui serviraient dans le terminal à masquer ou à altérer ce qui est collé ; l’utilisateur voit en tout cas la commande finale avant de l’insérer, s’il y a des paramètres.

5.9 Importation depuis ~/.ssh/config

Le fichier est lu et interprété (y compris des Include imbriqués avec une limite de profondeur et une détection des cycles) mais rien n’est exécuté : ProxyCommand n’est pas pris en charge, l’hôte est signalé comme non importable et l’app ne lance jamais cette commande.

5.10 Tunnels et proxy

Par défaut, l’écoute se fait uniquement sur 127.0.0.1. Si l’on choisit une adresse accessible depuis le réseau, l’app affiche un avertissement dans la boîte de dialogue et dans la liste (avec SOCKS5, ce serait un proxy ouvert à tous, sans authentification). Un tunnel local à l’écoute sur 127.0.0.1 est de toute façon accessible à n’importe quel programme du même ordinateur (autres utilisateurs compris, sur les systèmes multi-utilisateurs) : c’est le comportement de ssh -L, il est documenté et il n’est pas atténué. Verrouiller le coffre n’arrête pas les tunnels.

5.11 La webview et le frontend

Le frontend est une page locale servie par le paquet de l’app, sans contenu distant, sans v-html/innerHTML/eval (vérifié), avec les outils de développement de Nuxt désactivés (ainsi que l’inspecteur de la webview, que Wails n’active que dans les builds de débogage). La sortie des terminaux passe par xterm.js en tant que texte. Il manque une Content-Security-Policy : aujourd’hui, la définir ne serait pas trivial parce que la page générée contient des scripts en ligne (carte des imports et données initiales) ; la rendre stricte demande des hachages ou des nonces. Elle est consignée comme amélioration (§9) : ce serait une défense en profondeur, pas la correction d’une faille connue.

5.12 Mises à jour et chaîne d’approvisionnement

Aucune mise à jour automatique et aucune télémétrie : le frontend ne charge aucune ressource distante et les seules connexions sortantes sont celles vers les hôtes SSH que l’utilisateur ouvre et, uniquement s’il l’active, vers le serveur de l’assistant IA (§5.13). Le seul client HTTP de l’app est internal/ai. Les dépendances Go et npm sont figées par les fichiers de verrouillage ; appimagetool est fixé par version et par somme de contrôle. Les builds de publication ne sont pas encore signées (macOS/Windows) : tant qu’elles ne le sont pas, l’intégrité du paquet téléchargé ne se vérifie qu’avec SHA256SUMS. Il n’existe pas encore d’analyse automatique des dépendances vulnérables : à ajouter (§9).

5.13 Assistant IA local

L’assistant (explique, de la description à la commande, résume, aide pour les snippets) est désactivé par défaut et ouvre un nouveau canal : il transmet du texte du terminal à un serveur de modèles choisi par l’utilisateur. Aucun modèle n’est inclus et aucune requête ne part sans que l’utilisateur l’ait vue et envoyée.

Où le texte peut aller.

  • Par défaut uniquement vers l’ordinateur de l’utilisateur : sont « locaux » seulement localhost et les adresses de loopback écrites en toutes lettres (127.x, ::1). Tout autre nom est considéré comme distant même s’il pointe aujourd’hui vers l’ordinateur local, parce que la résolution des noms peut changer (DNS rebinding).
  • Un serveur non local ne peut être utilisé qu’après une confirmation explicite, liée à ce nom et à ce port : changer de serveur annule la confirmation. Avec un serveur non local et une clé, https:// est obligatoire.
  • Au moment de la connexion, pour les serveurs locaux, l’adresse effective doit être une adresse de loopback, sinon la connexion est refusée. Les redirections ne sont pas suivies et les variables d’environnement des proxys sont ignorées.
  • La clé du serveur, s’il y en a une, est chiffrée dans la base de données (AES-GCM avec une AAD dédiée), ne sort jamais vers l’interface et n’est pas envoyée à un serveur autre que celui configuré. La configuration n’est pas synchronisée.

Ce qui part. Chaque requête passe par un aperçu construit par le backend : le texte affiché est celui qui part, octet pour octet (testé), vers la destination affichée (hôte, port, modèle) ; si les réglages changent après l’aperçu, l’envoi est refusé. Avant l’aperçu, le texte et la requête passent par un filtre qui masque les clés privées, les identifiants dans les adresses, les en-têtes d’autorisation, les jetons connus (GitHub, AWS, Slack, JWT…), les valeurs de variables portant des noms comme password, secret, token, api_key, les options de ligne de commande (--password, sshpass -p, mysql -p…, curl -u) et les mots de passe et phrases secrètes enregistrés dans le coffre s’ils apparaissent dans le texte. L’aperçu indique ce qui a été masqué. Du terminal, il part au maximum la fin de 12 000 caractères.

Limites du filtre (déclarées aussi dans l’aperçu). Il reconnaît les secrets par leur forme et par leur valeur connue : il ne peut pas reconnaître un mot de passe écrit en clair sans un nom qui le révèle, un secret qui n’est pas dans le coffre, ni des informations sensibles qui ne sont pas des secrets (noms d’hôtes, adresses IP, chemins, contenu des fichiers, données personnelles dans la sortie). Avec un serveur non local, tout cela sort de l’ordinateur. C’est pourquoi l’aperçu doit être lu.

Programmes que l’app lance (détection des serveurs). Le bouton Démarrer fait exécuter par l’app un programme externe, c’est donc un point à tenir étroitement : seuls ollama serve et lms server start|stop sont exécutés, recherchés par nom fixe dans le PATH et dans des chemins connus ; aucune commande, aucun argument ni aucun chemin ne provient de l’utilisateur, de l’interface ou du modèle. Ollama est lancé avec OLLAMA_HOST=127.0.0.1:porta imposé, de sorte qu’il ne puisse pas se mettre à l’écoute sur le réseau ; les processus lancés par l’app se terminent avec l’app et Arrêter ne concerne qu’eux. Risque résiduel : qui peut déjà placer un exécutable nommé ollama dans le PATH de l’utilisateur obtient l’exécution lorsque l’utilisateur appuie sur Démarrer (même niveau que celui qui peut déjà modifier l’environnement de l’utilisateur, §2). Le bouton Télécharger ouvre dans le navigateur une page choisie dans une table fixe du backend (HTTPS, trois adresses officielles) : l’app ne télécharge ni n’installe rien et ne fait aucune connexion vers Internet ; ce qui est téléchargé et installé est décidé par l’utilisateur dans le navigateur. Les commandes de lms n’ont pas encore été essayées sur un vrai LM Studio.

Le modèle est une entité non fiable.

  • Sortie de terminal hostile (prompt injection) : un serveur ou un fichier peut faire afficher un texte qui « donne des ordres » au modèle. Atténuations : le texte est enfermé entre des délimiteurs qu’il ne peut pas refermer (ils sont neutralisés) et les instructions disent de le traiter comme une donnée ; le modèle n’a ni outils ni accès au système, il ne peut donc que produire du texte. Il reste possible que l’explication soit trompeuse ou que la commande proposée soit dangereuse.
  • Commande proposée : elle n’est jamais exécutée. Elle est insérée dans le prompt comme un collage (sans Entrée) et c’est l’utilisateur qui la lance ; l’interface affiche la commande dans un encadré, avertit si elle comporte plusieurs lignes (le shell pourrait ne pas prendre en charge le collage protégé) et en retire tout caractère de contrôle, ESC compris, afin qu’une réponse hostile ne puisse pas fermer le collage protégé et faire exécuter autre chose. Le risque résiduel est que l’utilisateur appuie sur Entrée sans lire.
  • Réponse excessive ou lente : plafond de 256 KB par réponse, interruption après 120 s sans données, au maximum 3 requêtes simultanées.

Verrouillage du coffre. Les requêtes en cours sont interrompues et les aperçus non encore envoyés sont oubliés.

Non vérifié : la compatibilité avec les vrais serveurs (Ollama, LM Studio, llama.cpp) est éprouvée avec un faux serveur qui imite l’API d’OpenAI, pas avec les vrais programmes ; les formats de streaming peuvent présenter des variantes imprévues. Le filtre n’a pas été évalué sur des sorties réelles de grande taille : les faux positifs (texte masqué sans raison) sont possibles et préférables aux faux négatifs.

6. Corrections apportées avec cette révision

#ProblèmeCorrectionPreuve
6.1Les paramètres Argon2id de vault.json et des enveloppes .shelleasy n’avaient qu’un minimum : un en-tête hostile pouvait demander des gigaoctets ou des heures de calcul avant le contrôle du mot de passePlafond : 16 passes, 1 GiB, 16 threads (vault.MaxTime/MaxMemoryKiB/MaxThreads, erreur ErrHugeParams), appliqué à la création, au déverrouillage, à l’export et à l’importTestParamsCeiling, TestHugeParamsRejectedOnOpen : le refus a lieu en moins d’une seconde, sans dériver de clés
6.2Noms de fichiers lus depuis un serveur SFTP utilisés sans contrôle pour construire des chemins locaux (sortie du dossier de destination sous Windows ; noms réservés ; flux alternatifs NTFS)unsafeName / safeReadDir / checkTopName : sont écartés "", ., .., /, NUL et, sous Windows, \ : * ? " < > |, les caractères de contrôle, les noms se terminant par un point ou une espace et les noms de périphériques (CON, NUL, COM1…) ; les noms écartés figurent dans les notes du transfertTestUnsafeName, TestSafeReadDirSkipsHostileNames
6.3Fichiers locaux créés avec les permissions 0644 pendant le téléchargementCréation avec 0600 ; les permissions de l’original s’appliquent une fois la copie terminéeTestLocalCreateIsPrivate

La suite Go complète passe aussi avec -race. Non vérifié : le comportement de 6.2 sous un vrai Windows (les règles Windows sont éprouvées avec une table, pas sur un système de fichiers Windows).

7. Hors du coffre : ce que l’app ne protège pas

  • Les données qui transitent dans les sessions (sortie du terminal, fichiers transférés, ce que l’on colle) : elles ne sont pas chiffrées au repos et dépendent de l’ordinateur et du serveur. Si l’on utilise l’assistant IA, le texte que l’on décide d’envoyer sort du coffre vers le serveur de modèles (§5.13). Le tampon de défilement du terminal et le shell local utilisent la mémoire et les fichiers du système d’exploitation.
  • Les fichiers téléchargés restent sur le disque de destination avec les protections de ce disque.
  • Le presse-papiers du système : un secret copié reste dans le presse-papiers tant que quelque chose ne le remplace pas.
  • Les exports .shelleasy sont protégés par leur mot de passe pendant toute leur durée de vie : conservez-les comme vous conserveriez des clés.

8. Revue externe

Recommandation : oui, avant la version 1.0 et avant de recommander l’app pour des identifiants de production, limitée à un périmètre petit et clair :

  1. internal/vault, internal/envelope, internal/store et le point de déverrouillage (internal/core/core.go) : environ 1 000 lignes, une revue de quelques jours.
  2. La gestion des clés des serveurs (internal/sshconn/hostkey.go) et des transferts (§5.7).
  3. L’intégration biométrique sous macOS (internal/biometric).

Il n’y a pas de budget prévu : des alternatives sans coût sont une revue par des pairs réalisée par des personnes qui connaissent Go et la cryptographie, la publication du code avec ce document pour susciter des signalements, et l’activation des alertes de sécurité de GitHub. Tant qu’une revue n’a pas été faite, la formulation honnête est : chiffrement avec des primitives standard et des choix documentés, non vérifié par des tiers.

9. Suivis ouverts

  • Rotation de la DEK lors du changement de mot de passe (facultatif) : rechiffrer la base de données et les secrets avec une nouvelle DEK, de sorte qu’une ancienne copie de vault.json plus l’ancien mot de passe n’ouvrent plus rien. C’est une opération longue et délicate (copie atomique, retour en arrière en cas d’erreur).
  • Détecter la restauration de copies anciennes avec une ancre dans le trousseau du système (§5.3).
  • Content-Security-Policy pour la webview (§5.11).
  • Analyse automatique des dépendances (govulncheck, pnpm audit) dans les contrôles (§5.12).
  • Signature des paquets et vérification de l’AppImage/Windows.
  • Windows Hello et déverrouillage biométrique sous Linux.