Sécurité

Ce que nous faisons réellement aujourd'hui, écrit par l'ingénieur qui l'a construit — sans badges de façade.

Chiffrement

  • TLS 1.2+ sur chaque connexion. Caddy, devant l’API, termine le TLS avec des certificats renouvelés automatiquement.
  • Les jetons de rafraîchissement OAuth des intégrations fabricants (GE / LG / Samsung / Frigidaire), les secrets TOTP et autres champs sensibles sont chiffrés individuellement avec AES-256-GCM. Chaque texte chiffré est lié à sa ligne via des données authentifiées supplémentaires (AAD) ; un échange de ligne falsifié échoue à la vérification du tag d'authentification.
  • La base SQLite locale de l'application mobile est chiffrée avec SQLCipher (AES-256). Les jetons sont stockés dans le trousseau sécurisé de la plateforme (iOS Keychain, Android Keystore) avec chiffrement matériel quand l'appareil le permet.
  • Les mots de passe sont hachés avec bcrypt (coût 12), pré-hachés en SHA-256 pour que les mots de passe de plus de 72 octets ne perdent pas d'entropie.

Authentification

  • Jetons d’accès JWT (TTL 1 heure) + jetons de rafraîchissement opaques (TTL 7 jours, renouvelés à chaque usage). Les jetons sont liés à un environnement précis via les revendications iss et aud.
  • La réutilisation d'un jeton de rafraîchissement déclenche la révocation immédiate de toutes les sessions actives de cet utilisateur — un jeton fuité et rejoué tue sa famille en une requête.
  • MFA TOTP optionnel avec codes de secours à usage unique. Les secrets MFA sont stockés chiffrés (voir Chiffrement ci-dessus).
  • Suspendre un compte ou changer le mot de passe inscrit tokens_invalidated_at sur la ligne utilisateur ; tout jeton d'accès émis avant ce moment est rejeté par le middleware à la requête suivante.
  • Connexion OAuth via Google et Apple. La revendication nonce d'Apple est vérifiée contre la valeur fournie par le client pour contrer le rejeu de jeton.

Journalisation d'audit

  • Chaque action sensible (connexion, changement de mot de passe, opération admin, changement d'état de commission liée à l'argent) écrit une ligne dans audit_logs.
  • La table est chaînée par hachage : chaque ligne stocke le SHA-256 de (hachage-précédent || charge utile de la ligne). Un vérificateur quotidien parcourt la chaîne ; toute rupture déclenche une alerte critique.
  • Le déclencheur de chaîne utilise un horodatage UTC canonique pour que l’écrivain et le vérificateur produisent le même hachage quel que soit le fuseau de session.
  • Le rôle API ne possède pas audit_logs et ne peut ni supprimer le déclencheur d'immuabilité ni DELETE des lignes. Le nettoyage de rétention s'exécute sous un rôle Postgres distinct.

Protections applicatives

  • Chaque requête de base de données touchant des données détenues par l'utilisateur est restreinte par user_id au niveau applicatif. Les clés étrangères du schéma reflètent cela : une clause de portée manquante échoue en mode fermé.
  • Les points de mutation acceptent une Idempotency-Key fournie par le client ; le point de création de cadeau partenaire l'exige, donc un double appui ne peut jamais produire deux débits.
  • Limites de débit par IP et par compte sur la connexion, le rafraîchissement, la réinitialisation de mot de passe et autres points sensibles, appliquées en mémoire dans le processus (express-rate-limit), avec des compteurs adossés à Postgres là où un magasin partagé est requis (OTP, idempotence, état OAuth). L'API tourne actuellement sur une seule réplique, donc ces limites par processus sont exactes pour cette topologie ; un magasin partagé serait introduit délibérément avant tout passage à l'échelle horizontale.
  • Protection CSRF sur le tableau de bord partenaire via cookie double-submit. Le proxy Next.js retire les cookies avant de transmettre à l'API et rejette les mutations cross-origin.
  • Le webhook d'abonnement RevenueCat vérifie un secret partagé à temps constant sur le corps brut avant tout traitement, et chaque événement n'est enregistré qu'une seule fois (déduplication idempotente) afin qu'un événement re-livré ou hors séquence ne puisse jamais s'appliquer deux fois.

Suppression de compte

  • La suppression est douce, avec une fenêtre de réflexion de 30 jours. Connectez-vous pendant la fenêtre pour récupérer ; après 30 jours, un cron efface définitivement le compte.
  • La purge récupère chaque objet détenu par l'utilisateur depuis le stockage d'objets (photos de reçus, documents, avatars) avant le déclenchement de la cascade SQL.
  • Les autorisations OAuth des intégrations fabricants (GE / LG / Samsung / Frigidaire) sont révoquées chez le fournisseur, pas seulement localement, à la déconnexion ou à la suppression du compte.
  • Les jetons de notification push sont supprimés à la déconnexion et à la suppression du compte.
  • L'événement de suppression définitive est lui-même enregistré dans la chaîne d'audit (avec l'e-mail de l'utilisateur conservé dans une colonne dénormalisée) pour pouvoir répondre plus tard à « avons-nous supprimé cet utilisateur, et quand ? ».

Hébergement

  • Nous tournons sur des droplets DigitalOcean que nous gérons directement. Postgres et MinIO (stockage objet compatible S3) vivent sur le même réseau privé derrière Caddy.
  • Nous n'opérons pas encore depuis plusieurs régions, ne faisons pas de restauration point-in-time, ni d'exercices formels de reprise après sinistre. Nous les ajouterons à mesure que nous grandirons ; nous préférons ne pas les revendiquer prématurément.
  • Nous ne détenons ni SOC 2, ni ISO 27001, ni aucune certification de sécurité tierce. Nous sommes une petite équipe qui y travaille à mesure que la base d'utilisateurs grandit.

Signaler une vulnérabilité

Écrivez à security@havenkeep.app avec les détails de tout problème de sécurité que vous trouvez. Nous n'avons pas encore de programme de bug-bounty rémunéré, mais nous répondrons personnellement sous quelques jours ouvrés et créditerons publiquement les divulgations responsables (avec votre permission).

Des questions ?

Nous serons ravis de détailler tout point précis.

Nous contacter