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

84 lines
2.5 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
#include "optimizersecret.h"
#include <QJsonArray>
#include <QJsonDocument>
namespace OptimizerSecret {
namespace {
// La forme que QJsonDocument donne au token DANS une chaîne : ["…"] sans ses bornes.
QByteArray formeEchappee(const QString &token)
{
const QByteArray tableau = QJsonDocument(QJsonArray{token}).toJson(QJsonDocument::Compact);
return tableau.mid(2, tableau.size() - 4);
}
QJsonValue masquerValeur(const QJsonValue &v, const QString &token, int *masques)
{
switch (v.type()) {
case QJsonValue::String:
return masquerTexte(v.toString(), token, masques);
case QJsonValue::Array: {
QJsonArray out;
for (const QJsonValue &x : v.toArray())
out.append(masquerValeur(x, token, masques));
return out;
}
case QJsonValue::Object:
return masquerJson(v.toObject(), token, masques);
default:
return v;
}
}
} // namespace
QString masquerTexte(const QString &texte, const QString &token, int *masques)
{
if (token.isEmpty())
return texte;
QString out = texte;
// L'échappée d'abord : elle CONTIENT la brute quand le token n'a ni " ni \ (elles sont alors
// égales), et sinon un optimiseur qui a recopié la forme sérialisée l'a fait sous celle-là.
for (const QString &forme : {QString::fromUtf8(formeEchappee(token)), token}) {
const int n = out.count(forme);
if (n == 0)
continue;
out.replace(forme, QString::fromUtf8(Masque)); // UTF-8 : « masqué »
if (masques)
*masques += n;
}
return out;
}
QJsonObject masquerJson(const QJsonObject &message, const QString &token, int *masques)
{
if (token.isEmpty())
return message;
QJsonObject out;
for (auto it = message.constBegin(); it != message.constEnd(); ++it)
out.insert(masquerTexte(it.key(), token, masques), masquerValeur(it.value(), token, masques));
return out;
}
QByteArray masquerLigneJson(const QByteArray &ligne, const QByteArray &token, int *masques)
{
if (token.isEmpty())
return ligne;
const QByteArray echappe = formeEchappee(QString::fromUtf8(token));
if (echappe.isEmpty() || !ligne.contains(echappe))
return ligne;
QByteArray out = ligne;
const int n = out.count(echappe);
out.replace(echappe, Masque);
if (masques)
*masques += n;
return out;
}
}