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 —
|
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 :
|
||||||
|
|||||||
@@ -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
|
// 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) --------------------------
|
||||||
|
|||||||
@@ -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">—</code>
|
<code id="settingLicenceMachine" style="font-family:ui-monospace,Consolas,monospace;user-select:all">—</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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user