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:
@@ -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 :
|
||||
|
||||
@@ -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.
|
||||
@@ -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) --------------------------
|
||||
|
||||
@@ -1009,6 +1009,10 @@
|
||||
<span>Installation ID</span>
|
||||
<code id="settingLicenceMachine" style="font-family:ui-monospace,Consolas,monospace;user-select:all">—</code>
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm is-danger" id="settingLicenceClear" hidden
|
||||
style="margin-top:8px">
|
||||
Deactivate (debug)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user