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>
84 lines
2.5 KiB
C++
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;
|
|
}
|
|
|
|
}
|