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
issetaud. - 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_atsur 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_logset 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_idau 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-Keyfournie 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).