Aller au contenu

Sécurité multi-tenant

Titreo E-Pay héberge les credentials de paiement de plusieurs marchands. La sécurité est donc multi-tenant par conception. Le code de crypto vit dans api/src/infrastructure/crypto/.

Les clés API Stripe/Adyen de chaque marchand sont stockées chiffrées en base, jamais en clair.

  • Algorithme : AES-256-GCM (chiffrement authentifié). api/src/infrastructure/crypto/aead.ts.
  • Clé maîtresse : la KEK (variable d’environnement PSP_CONFIG_KEK).
  • Stockage : FirebirdMerchantPspConfigRepo chiffre à l’écriture, déchiffre à la lecture (api/src/infrastructure/db/firebird-merchant-psp-config-repo.ts, migrations 003/005/020).

Les champs sensibles de l’outbox (ex. CVV de carte cadeau, tokens) sont également chiffrés via payload-crypto.ts avant insertion, avec des données associées (AAD) liées à l’identifiant de la ligne.

  • Format : tep_live_<aléatoire>. Génération : api/src/infrastructure/crypto/api-key.ts.
  • Stockage hashé : seul le hash est en base (api_keys, migration 004) — une fuite de la base ne révèle pas les clés.
  • En-tête : Authorization: Bearer tep_live_…. Vérification par le use case authenticate-api-key.ts, appelé depuis le plugin api/src/http/plugins/auth.ts.
  • Scopes : chaque clé porte des permissions (sessions:read, sessions:write, psp:write, webhooks:write, admin). Le plugin require-scope.ts rejette en 403 si le scope manque.

Au-delà des API keys (intégrations serveur), il existe :

  • des tokens de session client (signés, courte durée) pour le navigateur — session-token.ts ;
  • des sessions utilisateur (cookies) pour les consoles admin/marchand — user-session-token.ts, plugin user-session.ts ;
  • du hachage de mots de passe par scryptscrypt-password-hasher.ts.

Quand l’API notifie le marchand (session.completed, session.failed…), elle signe la requête :

x-titreo-signature: t=<timestamp>,v1=<hex(HMAC-SHA256(timestamp.payload))>

Le marchand recalcule le HMAC avec son secret partagé et compare (comparaison à temps constant). Les SDK fournissent un vérificateur prêt à l’emploi (verifyMerchantWebhook côté JS, Webhook\Verifier côté PHP). Une tolérance sur le timestamp (~5 min) limite le rejeu.

4. Webhooks PSP entrants : la bonne URL au bon marchand

Section intitulée « 4. Webhooks PSP entrants : la bonne URL au bon marchand »

Un PSP poste sur POST /v1/webhooks/:providerType/:merchantId. L’API résout le secret de webhook du marchand visé puis vérifie la signature avant de parser le corps (verifyWebhook de l’adapter PSP). Pas de signature valide → 400, sans effet de bord.

Le bootstrap navigateur (GET /v1/sessions/:id/bootstrap) renvoie un manifest signé par une clé privée Ed25519 (asymétrique). api/src/infrastructure/crypto/ed25519-signer.ts. La clé publique est embarquée dans le loader, qui refuse de charger des adapters dont le manifest n’est pas correctement signé. Chaque module d’adapter porte aussi une intégrité SRI (sha384-…).

  • url-guard.ts : empêche l’envoi de webhooks vers des IP privées en production (anti-SSRF).
  • Journaux : pino, avec masquage (redact) des champs sensibles. Jamais de PAN, CVV, carte cadeau en clair ni token PSP dans les logs (voir CONVENTIONS.md).
  • config/index.ts est le seul endroit qui lit process.env, validé par Zod ; le reste du code reçoit un objet Config typé par injection.
flowchart TB
  subgraph repos["Au repos (base)"]
    A["Configs PSP — AES-256-GCM (KEK)"]
    B["API keys — hash"]
    C["Mots de passe — scrypt"]
  end
  subgraph transit["En transit"]
    D["Webhooks sortants — HMAC-SHA256"]
    E["Webhooks PSP entrants — signature PSP"]
    F["Manifest navigateur — Ed25519 + SRI"]
  end

Source de vérité complémentaire : section « Sécurité multi-tenant » de ECOSYSTEM.md, et doc/site/content/security.mdx.