From 253e81252cd274e9ec1d76a6831e1b12f1ef4a6b Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sat, 5 Sep 2026 18:06:45 +0100 Subject: [PATCH] docs(licence): scenario d'essai, et le bouton qui le rend jouable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LICENCE-TESTS.md : cinq series. B eprouve ce qui doit tomber (chaine quelconque, licence tronquee, charge utile trafiquee, autre poste, autre paire de cles), C le risque symetrique et plus couteux — refuser un client qui a paye (espaces colles, retour a la ligne, redemarrage machine). D est dediee a la stabilite de l'empreinte, le defaut deja rencontre. E delimite la portee de l'activation. Le plan citait cmd://licence/clear sans moyen de l'appeler : un bouton "Deactivate (debug)" au bas de Settings -> Licence, revele seulement quand config.txt existe. Il donne au passage son unique appelant a ClearLicence, qui etait du code mort. Le document dit aussi ce qu'aucun essai ne prouve : rapiecer l'exe reste possible, il n'y a pas de revocation, et le brouillage n'est pas du chiffrement. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 8 ++ LICENCE-TESTS.md | 155 +++++++++++++++++++++++++++++++++++ delphi-backend/UMainForm.pas | 14 +++- index.html | 4 + js/app.js | 26 +++++- 5 files changed, 205 insertions(+), 2 deletions(-) create mode 100644 LICENCE-TESTS.md diff --git a/CLAUDE.md b/CLAUDE.md index 2ca9eda..9305aac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -417,6 +417,14 @@ sur une licence acceptee — pas de bouton « plus tard ». glisser une casse l'inference des methodes anonymes de tout ce qui suit — cinq `TThread.Queue` sans rapport se sont mis a ne plus compiler. +**Scenario d'essai** : [LICENCE-TESTS.md](LICENCE-TESTS.md) — cinq series, dont +une (D) dediee a la stabilite de l'empreinte, qui est le defaut deja rencontre. +La remise a zero passe par le bouton **Deactivate (debug)** au bas de +Settings -> Licence, revele seulement quand `config.txt` existe a cote de l'exe +(`cmd://licence/clear`, inerte sans lui). Il donne aussi son unique appelant a +`PM.Licence.ClearLicence`, et evite d'avoir a supprimer `prefs.bin` — qui +desactive mais emporte le mode de deverrouillage, le nom retenu et la sync. + **ORDRE OBLIGATOIRE.** Tant que `LicencePublicKeyBlob` est a zero, aucune licence n'est valide et l'ecran d'activation est **infranchissable** — l'exe construit ne s'ouvre plus. Poser la cle publique AVANT de deployer : diff --git a/LICENCE-TESTS.md b/LICENCE-TESTS.md new file mode 100644 index 0000000..e476c1a --- /dev/null +++ b/LICENCE-TESTS.md @@ -0,0 +1,155 @@ +# Système de licences — scénario d'essai + +Écrit le **2026-09-05**, après la séance qui a posé la vérification hors ligne, +le liage au poste et l'écran d'activation. + +Ce plan ne cherche pas à montrer que ça marche : **il cherche les endroits où ça +casserait sans prévenir**. Un système de licences échoue de deux façons +opposées, et les deux coûtent cher — refuser un client qui a payé, ou ouvrir à +quelqu'un qui n'a rien. Chaque essai ci-dessous vise l'une des deux. + +Ce que la séance a déjà appris, et qui vaut d'être gardé en tête : **l'empreinte +du poste dépendait de l'ordre de démarrage.** Selon que WMI répondait ou non, +la même machine se présentait sous deux identités et refusait sa propre licence. +Rien dans une utilisation normale ne l'aurait montré. D'où la série D. + +--- + +## Préparation + +**L'outil de remise à zéro** : pose un fichier `config.txt` à côté de l'exe +(n'importe quel contenu, même vide). Un bouton rouge **Deactivate (debug)** +apparaît alors au bas de *Settings → Licence* : il efface la licence rangée et +rien d'autre. Sans `config.txt` le bouton n'apparaît pas, et la commande +sous-jacente est de toute façon inerte — un exe livré ne peut pas être +désactivé par ce chemin. + +Ne supprime `%LOCALAPPDATA%\PMServer\prefs.bin` qu'en dernier recours : ça +désactive aussi, mais emporte le mode de déverrouillage, le nom retenu et les +réglages de sync. + +**Ce qu'il faut sous la main** : + +- ta licence valable, liée à ce poste ; +- l'Installation ID de ce poste (Settings → Licence, ou l'écran d'activation) ; +- `tools/licence.ps1` et la clé privée. + +**Ordre** : `A → B → C → D → E`. La série A établit l'état de référence ; les +suivantes le cassent volontairement. + +--- + +## A. Le chemin normal + +1. Coffre déjà activé, lancer l'application. + **À voir** : l'écran d'activation **n'apparaît pas**. On tombe directement + sur l'écran de connexion. +2. Settings → Licence. + **À voir** : *Licensed to*, *Email*, *Issued* et *Installation ID* remplis. + Le nom affiché est celui que tu as tapé à l'émission — il vient de la charge + utile signée, pas d'un réglage local. +3. Fermer, relancer. + **À voir** : toujours pas d'écran d'activation. C'est ce qui prouve que la + licence est rangée et relue, pas seulement acceptée une fois. + +--- + +## B. Ce qui doit être refusé + +Chaque essai part de l'écran d'activation (licence effacée). + +1. **Chaîne quelconque** : taper `nimportequoi`. + **À voir** : *This licence key is not valid*, **en rouge**. L'écran reste. +2. **Licence tronquée** : coller ta licence valable en retirant les dix derniers + caractères. + **À voir** : même refus. La signature ne tolère pas un octet. +3. **Charge utile trafiquée** : prendre ta licence, changer **un seul caractère + dans la partie avant le point** (le nom, l'adresse), coller. + **À voir** : même refus. C'est l'essai qui prouve que le contenu affiché dans + Settings ne peut pas être maquillé. +4. **Licence d'un autre poste** : + ``` + tools\licence.ps1 -For "x@y.z" -Name "Autre" -Machine "AAAA-BBBB-CCCC-DDDD" + ``` + coller le résultat. + **À voir** : refus. **C'est le cœur du liage** — une licence parfaitement + signée, mais pas pour cette machine. +5. **Licence d'une autre paire de clés** : + ``` + tools\licence.ps1 -Init -KeyFile "%TEMP%\faux.key" + tools\licence.ps1 -For "x@y.z" -Name "Faux" -Machine "" -KeyFile "%TEMP%\faux.key" + ``` + coller le résultat. + **À voir** : refus. **C'est le cœur de la signature** — la bonne machine, la + bonne forme, mais pas ta clé. Si cet essai passait, n'importe qui pourrait + fabriquer des licences. + Puis supprimer `%TEMP%\faux.key`. +6. **Ta vraie licence**, pour finir. + **À voir** : l'écran disparaît, on passe à la connexion. + +--- + +## C. Ce qui ne doit PAS être refusé + +Le risque symétrique, et le plus coûteux commercialement. + +1. Coller ta licence **avec des espaces avant et après** (une sélection à la + souris en attrape presque toujours). + **À voir** : acceptée. Le code fait `Trim`. +2. Coller ta licence **avec un retour à la ligne** collé au bout. + **À voir** : acceptée. +3. Après activation, **redémarrer la machine** (pas seulement l'application). + **À voir** : toujours activée. Le port du serveur change à chaque + lancement — si la licence avait vécu dans `localStorage`, elle serait + perdue ici. C'est l'essai qui prouve le stockage DPAPI. + +--- + +## D. L'empreinte du poste — la série qui a déjà attrapé un défaut + +1. Noter l'Installation ID dans Settings → Licence. +2. Fermer, relancer, relire. + **À voir** : **exactement le même**. Un seul caractère de différence, et + toutes les licences de cette machine tomberaient au prochain lancement. +3. Relancer **cinq fois de suite**, en laissant la machine chargée (ouvrir + quelques applications) pour varier les temps de démarrage. + **À voir** : le même à chaque fois. C'est le défaut corrigé le 2026-09-05 : + l'empreinte dépendait de l'initialisation de COM par WebView2, donc de la + vitesse de démarrage. +4. **Démarrage à froid** : redémarrer Windows, lancer l'application en premier. + **À voir** : le même. + +--- + +## E. La portée de l'activation + +1. **Autre compte Windows** sur la même machine : ouvrir une session + différente, lancer l'application. + **À voir** : l'écran d'activation **réapparaît**, avec le **même** + Installation ID. Comportement voulu : la licence est rangée par utilisateur + (DPAPI), l'empreinte est celle de la machine. Ta licence y sera acceptée. +2. **Copier l'exe dans un autre dossier** et le lancer. + **À voir** : toujours activée. La licence ne vit pas à côté de l'exe. + +--- + +## Ce qu'aucun de ces essais ne prouve + +À dire clairement, pour ne pas se croire protégé de ce qu'on ne l'est pas : + +- **Rapiécer l'exe pour sauter la vérification reste possible.** C'est vrai de + tout schéma hors ligne. La signature empêche de *fabriquer* des licences, pas + de contourner le code qui les lit. +- **Il n'y a pas de révocation.** Une licence émise vaut pour toujours sur sa + machine. La seule annulation possible est de régénérer la paire et de + réémettre — ce qui casse toutes les autres. +- **Le brouillage des blocs embarqués n'est pas du chiffrement.** L'onglet + *Memory* de Process Explorer, un débogueur ou la réponse HTTP rendent toujours + le frontend en clair. + +## Le défaut à reconnaître sans le provoquer + +Si `$ObfSecret` (BuildAssets.ps1) et `AssetObfSecret` (PM.EmbeddedAssets.pas) +divergent, la magie `PMA1` tombe et le serveur **refuse de servir** : +`/index.html` en **404**, fenêtre vide. Ce n'est pas une panne de licence, c'est +un désaccord de build. Symptôme à ne pas confondre. diff --git a/delphi-backend/UMainForm.pas b/delphi-backend/UMainForm.pas index 1564991..484cbb8 100644 --- a/delphi-backend/UMainForm.pas +++ b/delphi-backend/UMainForm.pas @@ -1336,6 +1336,17 @@ begin // // La cle est du base64url plus un point : que des caracteres surs dans une // URL, donc rien a encoder ni a decoder de special sur ce trajet. + // Desactiver pour eprouver l'activation. Garde par la presence de + // config.txt, comme Ctrl+Shift+D : un exe livre n'en a pas, la commande y + // est donc inerte. L'alternative — supprimer prefs.bin — emporterait aussi + // le mode de deverrouillage, le nom retenu et les reglages de sync. + else if ACmd = 'licence/clear' then + begin + if not FileExists('config.txt') then Exit; + PM.Licence.ClearLicence; + LogLine('Licence cleared (debug)'); + end + else if (ACmd = 'licence/status') or (ACmd = 'licence/activate') then begin var LLicOk: Boolean; @@ -1372,7 +1383,8 @@ begin ',"' + LFields[0] + '","' + PM.Licence.ThisMachineId + '","' + LFields[1] + - '","' + LFields[2] + '")'); + '","' + LFields[2] + + '",' + BoolToStr(FileExists('config.txt'), True).ToLower + ')'); end // ---- Start with Windows (HKCU Run registry) -------------------------- diff --git a/index.html b/index.html index 7796213..b14b078 100644 --- a/index.html +++ b/index.html @@ -1009,6 +1009,10 @@ Installation ID — + diff --git a/js/app.js b/js/app.js index c3a8f52..c28c23e 100644 --- a/js/app.js +++ b/js/app.js @@ -315,7 +315,7 @@ const Bridge = (() => { }, 3000); }); }, - onLicenceResult(ok, holder, machine, email, issued) { + onLicenceResult(ok, holder, machine, email, issued, debug) { const r = licenceResolver; if (!r) return; licenceResolver = null; @@ -325,8 +325,16 @@ const Bridge = (() => { machine: machine || '', email: email || '', issued: issued || '', + debug: !!debug, }); }, + // Desactive la licence pour eprouver l'activation. Cote Delphi la + // commande est inerte sans config.txt — le bouton qui l'appelle n'est + // d'ailleurs revele que dans ce cas. + clearLicence() { + if (!active) return; + cmd('cmd://licence/clear'); + }, onPrefResult(key, value) { const r = prefResolvers[key]; if (r) { @@ -6958,6 +6966,10 @@ function applyLicenceInfo(st) { $('#settingLicenceEmail').textContent = st.email || '—'; $('#settingLicenceIssued').textContent = st.issued || '—'; $('#settingLicenceMachine').textContent = st.machine || '—'; + // Reservee aux essais : elle renvoie a l'ecran d'activation sans emporter + // le reste des preferences, contrairement a supprimer prefs.bin. + const deact = $('#settingLicenceClear'); + if (deact) deact.hidden = !st.debug; } async function ensureLicence() { @@ -8206,6 +8218,18 @@ async function init() { const lbl = autofillComboLabel(state.autofillHotkeyFull); toast(state.autofillEnabled ? ('Autofill enabled (' + lbl + ')') : 'Autofill disabled'); }); + $('#settingLicenceClear').addEventListener('click', async () => { + const ok = await confirmDialog({ + title: 'Deactivate licence?', + body: 'The activation screen will come back on next launch. ' + + 'Your vault is untouched. Debug builds only.', + okText: 'Deactivate', + danger: true, + }); + if (!ok) return; + Bridge.clearLicence(); + toast('Licence cleared — restart to see the activation screen'); + }); $('#settingAutofillClear').addEventListener('change', e => { state.autofillClearField = e.target.checked; localStorage.setItem('autofillClearField', e.target.checked ? '1' : '0');