Patrick Schurig 693e01579f fix(optimiseur): un token recopié par l'optimiseur ne sort par aucun journal (brief 4 T7, correctif A)
Constat de T6 (question 2) : la trace masquait un token recopié dans reason ou planId, mais
les lignes [Optimiseur] (planId, texte, détail des refus, nom du handshake) et
GetOptimizerStatus.name le portaient en clair.

- OptimizerSecret : UNE fonction de masquage (texte brut et échappé JSON, objet JSON à toute
  profondeur, clés comprises ; ligne de trace sérialisée), partagée par le client et les traces.
- OptimizerClient masque chaque message reçu dès son analyse, avec le token envoyé sur cette
  connexion : rien en aval (journal, traces, app) ne voit le secret ; dit une fois.
- Test testAnEchoedTokenLeaksNowhere : secret avec " et \ recopié partout ; aucune ligne de
  journal (tous niveaux), aucune trace, aucune réponse RPC. 4 contre-épreuves mordent.

decisions sans endpoint : IDENTIQUE (23 792 lignes), 0 rejeu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 13:10:15 +02:00

57 lines
2.7 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
#pragma once
#include <QByteArray>
#include <QJsonObject>
#include <QString>
/*!
* \file optimizersecret.h
* \brief Masquage du token d'optimiseur (brief 4 T7, correctif A) — LA fonction partagée par le
* journal, les traces et tout ce qui part vers l'app.
*
* Le token ne voyage que dans le handshake (§3). Un optimiseur qui le RECOPIE dans ce qu'il
* renvoie (\c reason, \c planId, un code, un paramètre, son \c name…) ferait sortir le secret par
* chacun des chemins qui relaient son texte : journal \c [Optimiseur], traces, \c GetOptimizerStatus.
* Plutôt que de masquer à chacun de ces chemins — et d'oublier le prochain —, le client masque
* chaque message REÇU dès son analyse (\c OptimizerClient), avant tout usage : rien en aval ne voit
* jamais le secret. L'écrivain des traces masque en plus la ligne sérialisée (défense en
* profondeur), avec la même fonction.
*
* \invariant Aucune fonction d'ici ne journalise, ni le token, ni sa longueur.
*/
namespace OptimizerSecret {
//! Ce qui remplace le token, partout.
constexpr const char *Masque = "[token masqué]";
/*!
* \brief Remplace dans un TEXTE chaque occurrence du token, sous sa forme brute et sous sa forme
* échappée JSON (\c " et \c \\ précédés d'un \c \\).
* \param texte Texte à masquer. \param token Le secret ; vide = rien à faire.
* \param[in,out] masques Incrémenté du nombre de remplacements (peut être \c nullptr).
* \return Le texte masqué.
*/
QString masquerTexte(const QString &texte, const QString &token, int *masques = nullptr);
/*!
* \brief Masque un message JSON reçu : chaque chaîne ET chaque clé d'objet, à toute profondeur.
* \param message Objet analysé. \param token Le secret ; vide = rien à faire.
* \param[in,out] masques Incrémenté du nombre de remplacements (peut être \c nullptr).
* \return L'objet masqué. Les nombres, booléens et la structure sont inchangés.
*/
QJsonObject masquerJson(const QJsonObject &message, const QString &token, int *masques = nullptr);
/*!
* \brief Masque une ligne JSON DÉJÀ SÉRIALISÉE (\c QJsonDocument, compacte) : le token y est cherché
* sous sa seule forme échappée — c'est ainsi, et seulement ainsi, qu'il y figurerait. Chercher la
* forme brute pourrait mordre sur la syntaxe de la ligne et la rendre illisible.
* \param ligne Ligne JSON. \param token Le secret, brut ; vide = rien à faire.
* \param[in,out] masques Incrémenté du nombre de remplacements (peut être \c nullptr).
* \return La ligne masquée.
*/
QByteArray masquerLigneJson(const QByteArray &ligne, const QByteArray &token, int *masques = nullptr);
}