feat(licence): ecran d'activation au demarrage
ensureLicence() en tete d'init(), avant tout le reste : un ecran d'authentification qui s'affiche puis se fait recouvrir donnerait l'impression que le coffre etait accessible. L'ecran reprend les classes de l'ecran d'authentification — zero style nouveau hormis un z-index. Le statut est demande deux fois avant de le montrer : un demarrage a froid lent manquerait la fenetre de reponse, et le silence se lirait comme un refus. Une cle refusee ne dit pas pourquoi, et le journal note le verdict sans jamais la cle. ATTENTION : tant que LicencePublicKeyBlob est a zero, l'ecran est infranchissable. Poser la cle publique avant de deployer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -286,6 +286,36 @@ tools\licence.ps1 -For "a@b.com" -Name "Ami" # une licence
|
|||||||
`VerifyAgainstKey` prend la cle publique en parametre exactement pour que
|
`VerifyAgainstKey` prend la cle publique en parametre exactement pour que
|
||||||
l'auto-test n'ait pas besoin d'un second jeu de constantes de production.
|
l'auto-test n'ait pas besoin d'un second jeu de constantes de production.
|
||||||
|
|
||||||
|
### Ecran d'activation au demarrage
|
||||||
|
|
||||||
|
`ensureLicence()` (js/app.js) est appele **en tete d'`init()`**, avant tout le
|
||||||
|
reste : un ecran d'authentification qui s'affiche puis se fait recouvrir
|
||||||
|
donnerait l'impression que le coffre etait accessible. Il ne rend la main que
|
||||||
|
sur une licence acceptee — pas de bouton « plus tard ».
|
||||||
|
|
||||||
|
- Balisage `#licenceScreen` dans index.html, **place apres `#authScreen`** et
|
||||||
|
reprenant ses classes (`.auth-screen`, `.auth-card`, `.auth-hint`,
|
||||||
|
`.auth-form`) : zero style nouveau, sauf `#licenceScreen { z-index: 300 }`
|
||||||
|
pour passer au-dessus du bandeau (100) et du voile d'attente (200).
|
||||||
|
- Pont : `cmd://licence/status` et `cmd://licence/activate?key=`, reponse par
|
||||||
|
`Bridge.onLicenceResult(ok, porteur)`. La cle est du base64url plus un point,
|
||||||
|
donc rien a encoder de special sur le trajet.
|
||||||
|
- **Le statut est demande deux fois** avant de montrer l'ecran : un demarrage a
|
||||||
|
froid lent peut manquer la fenetre de 3 s, et le silence se lirait comme un
|
||||||
|
refus — presenter l'activation a quelqu'un qui a deja paye coute plus cher
|
||||||
|
qu'un second appel.
|
||||||
|
- Une cle refusee ne dit **pas pourquoi** : signature invalide, recopie fautive
|
||||||
|
et charge utile trafiquee se ressemblent, et detailler n'aiderait que celui
|
||||||
|
qui essaie d'en fabriquer une. Le journal note le verdict, **jamais la cle**.
|
||||||
|
- Hors du navigateur hote (`Bridge.active` faux : dev en navigateur, harnais de
|
||||||
|
test) il n'y a pas de licence a verifier — la porte est ouverte.
|
||||||
|
|
||||||
|
**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 :
|
||||||
|
`-Init` → coller les 72 octets → reconstruire → `-For` pour s'emettre une
|
||||||
|
licence a soi.
|
||||||
|
|
||||||
**Ce que ca arrete** : sans la cle privee, personne ne fabrique de licence
|
**Ce que ca arrete** : sans la cle privee, personne ne fabrique de licence
|
||||||
valable. **Ce que ca n'arrete pas** : rapiecer l'exe pour sauter l'appel — vrai
|
valable. **Ce que ca n'arrete pas** : rapiecer l'exe pour sauter l'appel — vrai
|
||||||
de tout schema hors ligne. La seule parade reelle serait de deporter une
|
de tout schema hors ligne. La seule parade reelle serait de deporter une
|
||||||
|
|||||||
@@ -236,6 +236,7 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
radial-gradient(ellipse at bottom right, rgba(167,139,250,0.08), transparent 50%),
|
radial-gradient(ellipse at bottom right, rgba(167,139,250,0.08), transparent 50%),
|
||||||
var(--bg);
|
var(--bg);
|
||||||
}
|
}
|
||||||
|
#licenceScreen { z-index: 300; }
|
||||||
.auth-card {
|
.auth-card {
|
||||||
width: 100%; max-width: 400px;
|
width: 100%; max-width: 400px;
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ uses
|
|||||||
{$IFDEF USE_EDGE_BROWSER}
|
{$IFDEF USE_EDGE_BROWSER}
|
||||||
FMX.TMSFNCEdgeWebBrowser,
|
FMX.TMSFNCEdgeWebBrowser,
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.PinUnlock, PM.UserPrefs, PM.AutoStart,
|
PM.HTTPServer, PM.Bridge, PM.QuickUnlock, PM.PinUnlock, PM.UserPrefs, PM.Licence, PM.AutoStart,
|
||||||
PM.Favicon,
|
PM.Favicon,
|
||||||
FMX.Platform.Win, FMX.Menus; // WindowHandleToPlatform → HWND for visibility check
|
FMX.Platform.Win, FMX.Menus; // WindowHandleToPlatform → HWND for visibility check
|
||||||
const
|
const
|
||||||
@@ -1330,6 +1330,38 @@ begin
|
|||||||
PM.UserPrefs.SetPref(LKey, GetParam('value'));
|
PM.UserPrefs.SetPref(LKey, GetParam('value'));
|
||||||
end
|
end
|
||||||
|
|
||||||
|
// ---- Licence hors ligne (PM.Licence) ---------------------------------
|
||||||
|
// cmd://licence/status -> Bridge.onLicenceResult(ok, porteur)
|
||||||
|
// cmd://licence/activate?key=<...> -> idem, apres verification + rangement
|
||||||
|
//
|
||||||
|
// 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.
|
||||||
|
else if (ACmd = 'licence/status') or (ACmd = 'licence/activate') then
|
||||||
|
begin
|
||||||
|
var LLicOk: Boolean;
|
||||||
|
var LLicInfo: TLicenceInfo;
|
||||||
|
if ACmd = 'licence/activate' then
|
||||||
|
begin
|
||||||
|
LLicOk := PM.Licence.ActivateLicence(GetParam('key'), LLicInfo);
|
||||||
|
// On journalise le VERDICT, jamais la cle : le journal se copie et
|
||||||
|
// s'envoie, et une licence valable est une valeur transmissible.
|
||||||
|
LogLine('Licence activation: ' + BoolToStr(LLicOk, True));
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
LLicOk := PM.Licence.IsLicensed;
|
||||||
|
LLicInfo := PM.Licence.LicenceInfo;
|
||||||
|
end;
|
||||||
|
var LHolder := LLicInfo.Holder;
|
||||||
|
if LHolder = '' then LHolder := LLicInfo.Email;
|
||||||
|
LHolder := StringReplace(LHolder, '\', '\\', [rfReplaceAll]);
|
||||||
|
LHolder := StringReplace(LHolder, '"', '\"', [rfReplaceAll]);
|
||||||
|
WebBrowser.ExecuteJavaScript(
|
||||||
|
'if(window.Bridge&&Bridge.onLicenceResult)' +
|
||||||
|
'Bridge.onLicenceResult(' + BoolToStr(LLicOk, True).ToLower +
|
||||||
|
',"' + LHolder + '")');
|
||||||
|
end
|
||||||
|
|
||||||
// ---- Start with Windows (HKCU Run registry) --------------------------
|
// ---- Start with Windows (HKCU Run registry) --------------------------
|
||||||
else if ACmd = 'autostart/get' then
|
else if ACmd = 'autostart/get' then
|
||||||
begin
|
begin
|
||||||
|
|||||||
+23
@@ -85,6 +85,29 @@
|
|||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
<!-- AUTH SCREEN (login + register) -->
|
<!-- AUTH SCREEN (login + register) -->
|
||||||
<!-- ============================================================ -->
|
<!-- ============================================================ -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<!-- LICENCE GATE (avant tout, cf. ensureLicence) -->
|
||||||
|
<!-- ============================================================ -->
|
||||||
|
<section id="licenceScreen" class="auth-screen is-hidden">
|
||||||
|
<div class="auth-card">
|
||||||
|
<div class="auth-brand">
|
||||||
|
<svg class="auth-brand-icon"><use href="#i-lock"/></svg>
|
||||||
|
<h1>Activate Vault</h1>
|
||||||
|
<p>Paste the licence key you were sent.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="licenceHint" class="auth-hint"></div>
|
||||||
|
|
||||||
|
<form id="licenceForm" class="auth-form" autocomplete="off">
|
||||||
|
<label class="field">
|
||||||
|
<span>Licence key</span>
|
||||||
|
<input id="licenceKey" type="text" spellcheck="false" required>
|
||||||
|
</label>
|
||||||
|
<button type="submit" class="btn btn-primary btn-block">Activate</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section id="authScreen" class="auth-screen">
|
<section id="authScreen" class="auth-screen">
|
||||||
<div class="auth-card">
|
<div class="auth-card">
|
||||||
<div class="auth-brand">
|
<div class="auth-brand">
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const API = (location.pathname.indexOf('/password-manager/') === 0)
|
|||||||
// Active only when running inside the Delphi-hosted WebView2 (API === '').
|
// Active only when running inside the Delphi-hosted WebView2 (API === '').
|
||||||
// Falls back to navigator.clipboard for the standalone PHP frontend.
|
// Falls back to navigator.clipboard for the standalone PHP frontend.
|
||||||
const prefResolvers = {};
|
const prefResolvers = {};
|
||||||
|
let licenceResolver = null;
|
||||||
const fileSaveResolvers = {};
|
const fileSaveResolvers = {};
|
||||||
const fileChunkResolvers = {};
|
const fileChunkResolvers = {};
|
||||||
// Stream a base64 string to Delphi in URL-sized pieces via cmd://file/chunk,
|
// Stream a base64 string to Delphi in URL-sized pieces via cmd://file/chunk,
|
||||||
@@ -293,6 +294,33 @@ const Bridge = (() => {
|
|||||||
cmd('cmd://prefs/set?key=' + encodeURIComponent(key) +
|
cmd('cmd://prefs/set?key=' + encodeURIComponent(key) +
|
||||||
'&value=' + encodeURIComponent(value || ''));
|
'&value=' + encodeURIComponent(value || ''));
|
||||||
},
|
},
|
||||||
|
// Licence check / activation, verified in Delphi against the
|
||||||
|
// publisher's public key (PM.Licence). Hors du navigateur hôte il n'y
|
||||||
|
// a pas de licence à vérifier — le harnais de test et le dev en
|
||||||
|
// navigateur passent.
|
||||||
|
// ponytail: un seul résolveur, pas une table comme prefResolvers — les
|
||||||
|
// deux appels ne se croisent jamais (statut au démarrage, activation
|
||||||
|
// sur clic). Passer à une table le jour où ils se croiseraient.
|
||||||
|
licence(action, key) {
|
||||||
|
if (!active) return Promise.resolve({ ok: true, holder: '' });
|
||||||
|
return new Promise(resolve => {
|
||||||
|
licenceResolver = resolve;
|
||||||
|
cmd('cmd://licence/' + action +
|
||||||
|
(action === 'activate' ? '?key=' + encodeURIComponent(key || '') : ''));
|
||||||
|
setTimeout(() => {
|
||||||
|
if (licenceResolver === resolve) {
|
||||||
|
licenceResolver = null;
|
||||||
|
resolve({ ok: false, holder: '' });
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onLicenceResult(ok, holder) {
|
||||||
|
const r = licenceResolver;
|
||||||
|
if (!r) return;
|
||||||
|
licenceResolver = null;
|
||||||
|
r({ ok: !!ok, holder: holder || '' });
|
||||||
|
},
|
||||||
onPrefResult(key, value) {
|
onPrefResult(key, value) {
|
||||||
const r = prefResolvers[key];
|
const r = prefResolvers[key];
|
||||||
if (r) {
|
if (r) {
|
||||||
@@ -6908,6 +6936,43 @@ function resetAutoLock() {
|
|||||||
}, { passive: true })
|
}, { passive: true })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// La porte d'entrée : sans licence valable, rien d'autre ne se peint.
|
||||||
|
// Ne rend la main QUE sur une licence acceptée — il n'y a pas de bouton
|
||||||
|
// "plus tard", c'est tout l'objet de l'écran.
|
||||||
|
async function ensureLicence() {
|
||||||
|
if ((await Bridge.licence('status')).ok) return;
|
||||||
|
// Un démarrage à froid lent peut manquer la fenêtre de réponse, et le
|
||||||
|
// silence se lit alors comme un refus : redemander une fois coûte moins
|
||||||
|
// qu'un écran d'activation présenté à quelqu'un qui a déjà payé.
|
||||||
|
if ((await Bridge.licence('status')).ok) return;
|
||||||
|
|
||||||
|
const screen = $('#licenceScreen');
|
||||||
|
const hint = $('#licenceHint');
|
||||||
|
const input = $('#licenceKey');
|
||||||
|
screen.classList.remove('is-hidden');
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
await new Promise(resolve => {
|
||||||
|
$('#licenceForm').addEventListener('submit', async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const key = input.value.trim();
|
||||||
|
if (!key) return;
|
||||||
|
hint.textContent = 'Checking…';
|
||||||
|
const res = await Bridge.licence('activate', key);
|
||||||
|
if (!res.ok) {
|
||||||
|
// Une clé refusée ne dit pas POURQUOI : signature invalide,
|
||||||
|
// recopie fautive et charge utile trafiquée se ressemblent, et
|
||||||
|
// détailler n'aiderait que celui qui essaie d'en fabriquer une.
|
||||||
|
hint.textContent = 'This licence key is not valid.';
|
||||||
|
input.select();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
screen.classList.add('is-hidden');
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function showAuth() {
|
async function showAuth() {
|
||||||
$('#authScreen').classList.remove('is-hidden');
|
$('#authScreen').classList.remove('is-hidden');
|
||||||
$('#appShell').classList.add('is-hidden');
|
$('#appShell').classList.add('is-hidden');
|
||||||
@@ -7353,6 +7418,10 @@ async function init() {
|
|||||||
history.replaceState(null, '', location.pathname + location.hash);
|
history.replaceState(null, '', location.pathname + location.hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avant tout le reste : un écran d'authentification qui s'affiche puis se
|
||||||
|
// fait recouvrir donnerait l'impression que le coffre était accessible.
|
||||||
|
await ensureLicence();
|
||||||
|
|
||||||
installCustomContextMenu();
|
installCustomContextMenu();
|
||||||
document.addEventListener('keydown', e => {
|
document.addEventListener('keydown', e => {
|
||||||
const mod = e.ctrlKey || e.metaKey;
|
const mod = e.ctrlKey || e.metaKey;
|
||||||
|
|||||||
Reference in New Issue
Block a user