docs(licence): scenario d'essai, et le bouton qui le rend jouable

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 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-09-05 18:06:45 +01:00
parent fc945ac6ab
commit 253e81252c
5 changed files with 205 additions and 2 deletions
+8
View File
@@ -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 — 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. 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 **ORDRE OBLIGATOIRE.** Tant que `LicencePublicKeyBlob` est a zero, aucune
licence n'est valide et l'ecran d'activation est **infranchissable** — l'exe 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 : construit ne s'ouvre plus. Poser la cle publique AVANT de deployer :
+155
View File
@@ -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 "<TON ID>" -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.
+13 -1
View File
@@ -1336,6 +1336,17 @@ begin
// //
// La cle est du base64url plus un point : que des caracteres surs dans une // 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. // 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 else if (ACmd = 'licence/status') or (ACmd = 'licence/activate') then
begin begin
var LLicOk: Boolean; var LLicOk: Boolean;
@@ -1372,7 +1383,8 @@ begin
',"' + LFields[0] + ',"' + LFields[0] +
'","' + PM.Licence.ThisMachineId + '","' + PM.Licence.ThisMachineId +
'","' + LFields[1] + '","' + LFields[1] +
'","' + LFields[2] + '")'); '","' + LFields[2] +
'",' + BoolToStr(FileExists('config.txt'), True).ToLower + ')');
end end
// ---- Start with Windows (HKCU Run registry) -------------------------- // ---- Start with Windows (HKCU Run registry) --------------------------
+4
View File
@@ -1009,6 +1009,10 @@
<span>Installation ID</span> <span>Installation ID</span>
<code id="settingLicenceMachine" style="font-family:ui-monospace,Consolas,monospace;user-select:all">&mdash;</code> <code id="settingLicenceMachine" style="font-family:ui-monospace,Consolas,monospace;user-select:all">&mdash;</code>
</div> </div>
<button class="btn btn-ghost btn-sm is-danger" id="settingLicenceClear" hidden
style="margin-top:8px">
Deactivate (debug)
</button>
</div> </div>
</div> </div>
</div> </div>
+25 -1
View File
@@ -315,7 +315,7 @@ const Bridge = (() => {
}, 3000); }, 3000);
}); });
}, },
onLicenceResult(ok, holder, machine, email, issued) { onLicenceResult(ok, holder, machine, email, issued, debug) {
const r = licenceResolver; const r = licenceResolver;
if (!r) return; if (!r) return;
licenceResolver = null; licenceResolver = null;
@@ -325,8 +325,16 @@ const Bridge = (() => {
machine: machine || '', machine: machine || '',
email: email || '', email: email || '',
issued: issued || '', 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) { onPrefResult(key, value) {
const r = prefResolvers[key]; const r = prefResolvers[key];
if (r) { if (r) {
@@ -6958,6 +6966,10 @@ function applyLicenceInfo(st) {
$('#settingLicenceEmail').textContent = st.email || '—'; $('#settingLicenceEmail').textContent = st.email || '—';
$('#settingLicenceIssued').textContent = st.issued || '—'; $('#settingLicenceIssued').textContent = st.issued || '—';
$('#settingLicenceMachine').textContent = st.machine || '—'; $('#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() { async function ensureLicence() {
@@ -8206,6 +8218,18 @@ async function init() {
const lbl = autofillComboLabel(state.autofillHotkeyFull); const lbl = autofillComboLabel(state.autofillHotkeyFull);
toast(state.autofillEnabled ? ('Autofill enabled (' + lbl + ')') : 'Autofill disabled'); 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 => { $('#settingAutofillClear').addEventListener('change', e => {
state.autofillClearField = e.target.checked; state.autofillClearField = e.target.checked;
localStorage.setItem('autofillClearField', e.target.checked ? '1' : '0'); localStorage.setItem('autofillClearField', e.target.checked ? '1' : '0');