# ADR-0006 : Stockage des tokens OAuth dans le trousseau OS

- **Date** : 2026-04-14
- **Statut** : Accepté

## Contexte

Depuis la v0.7.0, Simpl'Résultat utilise OAuth2 Authorization Code + PKCE pour authentifier les utilisateurs auprès de Logto (Compte Maximus). Les tokens résultants (`access_token`, `refresh_token`, `id_token`) étaient persistés dans `<app_data>/auth/tokens.json`, protégés uniquement par les permissions fichier (`0600` sous Unix, aucune ACL sous Windows).

Le refresh token donne une session longue durée. Le laisser en clair expose l'utilisateur à plusieurs classes d'attaques :

- Malware local tournant sous le même UID (lecture du home directory).
- Backups automatiques (home sync, backup tools) qui copient le fichier sans distinction.
- Shell non-root obtenu par l'attaquant.
- Sous Windows, absence de protection ACL rendait le fichier lisible par n'importe quel process utilisateur.

De plus, avant cette ADR, `account.json` était également en clair et son champ `subscription_status` servait au gating licence (Premium). Écrire manuellement `{"subscription_status": "active"}` dans ce fichier contournait le paywall.

## Options considérées

### Option 1 — Trousseau OS via `keyring` crate (RETENUE)

Librairie Rust `keyring` (v3.6) qui expose une API unifiée au-dessus des trousseaux natifs :
- **Windows** : Credential Manager (Win32 API, toujours présent)
- **Linux** : Secret Service via D-Bus (gnome-keyring, kwallet, keepassxc)
- **macOS** : Keychain Services (hors cible actuelle)

**Avantages** :
- Le système d'exploitation gère la clé maître (session utilisateur).
- Pas de mot de passe supplémentaire à demander à l'utilisateur.
- Support multi-plateforme natif.

**Inconvénients** :
- Sur Linux, requiert un service Secret Service actif (D-Bus + keyring daemon). Sur une session headless ou un CI sans D-Bus, il faut un fallback.
- Dépendance de build supplémentaire (`libdbus-1-dev`).

### Option 2 — `tauri-plugin-stronghold`

Chiffrement au repos avec une master password déverrouillée au démarrage.

**Rejeté** parce que :
- Casse l'UX de connexion silencieuse (refresh automatique au démarrage).
- Ajoute une saisie de passphrase à chaque lancement.
- Surface de UX plus large qu'un simple fallback keychain.

### Option 3 — Chiffrement AES-256-GCM custom avec clé dérivée du PIN

**Rejeté** parce que :
- Seulement applicable aux profils protégés par PIN (minorité).
- Les tokens OAuth doivent être lus sans interaction (refresh silencieux), donc aucune clé à demander.
- Simplement déplace le problème de la clé maître.

## Décision

**Trousseau OS via `keyring` crate, avec fallback fichier supervisé.**

### Architecture

Nouveau module `src-tauri/src/commands/token_store.rs` qui centralise le stockage :

- `save(app, &StoredTokens)` — tente le keychain, retombe sur `tokens.json` chiffré par permissions (`0600` Unix) si keychain indisponible.
- `load(app) -> Option<StoredTokens>` — lit depuis le keychain, migre un `tokens.json` résiduel à la volée.
- `delete(app)` — efface les deux stores (idempotent).
- `store_mode(app) -> Option<StoreMode>` — exposé au frontend pour afficher une bannière quand le fallback est actif.

### Choix du backend `keyring`

Le crate `keyring` v3 demande la sélection explicite des backends :

- **Linux** : `sync-secret-service` + `crypto-rust` → passe par le crate `dbus-secret-service` → crate `dbus` → lib C `libdbus-1`. Requiert `libdbus-1-dev` au build time et `libdbus-1-3` au runtime (ce dernier est universellement présent sur les distributions desktop Linux).
- **Windows** : `windows-native` → bind direct sur `windows-sys`, pas de dépendance externe.

L'option `async-secret-service` (via `zbus`, pur Rust) a été envisagée pour éviter la dépendance `libdbus-1-dev`, mais elle force une API asynchrone sur toutes les plateformes, ce qui ne matche pas le design sync de `license_commands::current_edition` (appelé depuis `check_entitlement`, sync). Le compromis accepté : un paquet apt de plus dans la CI, une API sync partout.

### Identité du trousseau

`service = "com.simpl.resultat"` (identifiant canonique de l'app dans `tauri.conf.json`), `user = "oauth-tokens"`. Ce choix aligne l'entrée keychain avec l'identité installée de l'app pour que les outils de management de credentials du système puissent la scoper correctement.

### Garde anti-downgrade

Un flag persistant `store_mode` (valeurs `keychain` ou `file`) est écrit dans `<app_data>/auth/store_mode` après chaque opération. Une fois qu'un `store_mode = keychain` a été enregistré, toute tentative ultérieure de sauvegarde qui échoue sur le keychain retourne une erreur au lieu de silenter-dégrader vers le fichier. Cela empêche un attaquant local de forcer la dégradation en bloquant temporairement D-Bus pour capturer les tokens en clair au prochain refresh.

### Migration transparente

Au premier `load()` après upgrade depuis v0.7.x, le module détecte `tokens.json` résiduel, copie son contenu dans le keychain, puis **overwrite le fichier avec des zéros + `fsync()` avant `remove_file()`**. C'est un mitigation best-effort contre la récupération des bits sur unallocated sectors (CWE-212). Pas un substitut à un disk encryption : les backups antérieurs à la migration conservent évidemment le vieux fichier. Documenté dans le CHANGELOG comme recommandation de rotation de session post-upgrade pour les utilisateurs inquiets.

### Scope : `tokens.json` migré, `account.json` signé

`account.json` **n'est pas** migré dans le keychain pour limiter le blast radius du changement et garder `write_restricted()` en place pour les fichiers non-sensibles. Toutefois, le champ `subscription_status` de ce cache servait au gating de licence Premium, ce qui créait un trou de tampering : un malware local pouvait écrire `"active"` dans le cache pour bypass le paywall sans jamais toucher le keychain.

**Corrigé via un second module `account_cache.rs`** : le cache est désormais encapsulé dans un wrapper `{"data": {...}, "sig": "<HMAC-SHA256>"}`. La clé HMAC 32 bytes est stockée dans le keychain (`user = "account-hmac-key"`), parallèlement aux tokens. Le chemin de gating (`license_commands::check_account_edition`) appelle `account_cache::load_verified` qui refuse tout payload non-signé ou avec signature invalide, et **fail-closed** (retourne None → Premium reste gated) si la clé HMAC est inaccessible.

Le chemin d'affichage UI (`get_account_info` → `load_unverified`) accepte encore les anciens payloads non-signés pour que les utilisateurs upgradés voient leur profil immédiatement. La distinction display/verified est explicite dans l'API.

## Conséquences

### Positives

- **Protection cryptographique native** : sous Windows, les tokens sont maintenant protégés par Credential Manager (DPAPI sous le capot). Sous Linux, par le keyring daemon avec une master password de session.
- **Anti-tampering du gating licence** : écrire `account.json` ne débloque plus Premium.
- **Fail-closed par défaut** : tous les chemins qui échouent sur le keychain retournent des erreurs au lieu de dégrader silencieusement.
- **Migration transparente** : zéro action utilisateur pour les upgrades depuis v0.7.x.
- **Anti-downgrade** : un attaquant ne peut pas forcer la dégradation vers le fichier pour capturer les tokens au prochain refresh.

### Négatives

- **Dépendance Linux** : `libdbus-1-dev` est requis au build time (ajouté à `check.yml` et `release.yml`). Au runtime, `libdbus-1-3` est déjà présent sur toutes les distros desktop, mais une session headless sans D-Bus déclenche le fallback fichier (signalé à l'utilisateur par la bannière Settings).
- **Surface d'attaque supply-chain accrue** : `keyring` + `dbus-secret-service` + `zbus` + `dbus` représentent une chaîne transitive nouvelle. Mitigé par un step `cargo audit` non-bloquant dans `check.yml`.
- **Logs plus verbeux** : chaque fallback imprime un warning sur stderr pour qu'un dev puisse diagnostiquer. Pas de télémétrie.
- **Sous Linux, la première utilisation peut demander un déverrouillage** : GNOME Keyring peut prompt l'utilisateur pour déverrouiller sa session keyring si elle ne l'est pas déjà. Ce comportement est natif du trousseau, pas de Simpl'Résultat.

### Tests

- Tests unitaires : 9 nouveaux tests (serde round-trip de `StoredTokens`, parse/encode de `StoreMode`, zéroïfication, HMAC sign/verify, tamper detection, wrong key, envelope serde).
- Tests manuels : matrice de 5 scénarios sur pop-os (Linux) et Windows, documentée dans l'issue #82.
- Pas de mock du keychain en CI : la matrice manuelle couvre les chemins où une lib externe est requise.

## Références

- Issue parente : maximus/simpl-resultat#66
- PRs : #83 (core), #84 (CI/packaging), #85 (subscription HMAC), #86 (UI banner), #87 (wrap-up)
- CWE-212 : Improper Removal of Sensitive Information Before Storage or Transfer
- CWE-312 : Cleartext Storage of Sensitive Information
- CWE-345 : Insufficient Verification of Data Authenticity
- CWE-757 : Selection of Less-Secure Algorithm During Negotiation
- [keyring crate v3.6 documentation](https://docs.rs/keyring/3.6.3/keyring/)
- [Secret Service API specification](https://specifications.freedesktop.org/secret-service-spec/latest/)
