diff --git a/.gitignore b/.gitignore index 31f34e7..2bf70bc 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,7 @@ android-client/web/*.res # il se sauvegarde ailleurs, il ne se versionne pas. *.jks *.keystore + +# Cle privee des licences. Qui la detient fabrique des licences valables : +# elle vit dans un gestionnaire de mots de passe, pas dans un depot. +tools/licence-private.key diff --git a/tools/licence.ps1 b/tools/licence.ps1 new file mode 100644 index 0000000..b275333 --- /dev/null +++ b/tools/licence.ps1 @@ -0,0 +1,96 @@ +# Fabrique des licences signees. A LANCER CHEZ TOI, JAMAIS LIVRE. +# +# .\tools\licence.ps1 -Init cree la paire de cles (une seule fois) +# et imprime la constante Delphi a coller +# .\tools\licence.ps1 -For "a@b.com" -Name "Ami" fabrique une licence +# +# POURQUOI ECDSA P-256 ET PAS AUTRE CHOSE : .NET sur Windows EST CNG. La +# signature que produit ce script (r||s brut, 64 octets) est exactement celle +# qu'attend BCryptVerifySignature cote Delphi, et les coordonnees exportees ici +# se rangent telles quelles dans un BCRYPT_ECCPUBLIC_BLOB. Aucun ASN.1 a lire +# d'un cote ni de l'autre - c'est ce qui rend la verification courte. + +param( + [switch]$Init, + [string]$For, + [string]$Name = '', + [string]$KeyFile = "$PSScriptRoot\licence-private.key" +) + +$ErrorActionPreference = 'Stop' +Add-Type -AssemblyName System.Security + +function ToB64Url([byte[]]$b) { + [Convert]::ToBase64String($b).TrimEnd('=').Replace('+', '-').Replace('/', '_') +} + +if ($Init) { + if (Test-Path $KeyFile) { + throw "$KeyFile existe deja. L'ecraser invaliderait TOUTES les licences deja emises. Deplace-le d'abord si c'est voulu." + } + + $ecdsa = New-Object System.Security.Cryptography.ECDsaCng 256 + $p = $ecdsa.ExportParameters($true) + + # La cle privee : D, plus le point public pour pouvoir re-signer sans le + # recalculer. 96 octets, en hexa pour rester lisible dans un gestionnaire + # de mots de passe - c'est la ou elle doit finir, pas dans ce dossier. + $priv = [byte[]]($p.D + $p.Q.X + $p.Q.Y) + [System.IO.File]::WriteAllText($KeyFile, ([BitConverter]::ToString($priv) -replace '-', '')) + + # BCRYPT_ECCPUBLIC_BLOB : magic ECS1 + longueur + X + Y. + $blob = [byte[]]([BitConverter]::GetBytes([uint32]0x31534345) + + [BitConverter]::GetBytes([uint32]32) + $p.Q.X + $p.Q.Y) + + '' + 'Cle privee ecrite dans : ' + $KeyFile + 'METS-LA A L ABRI ET SUPPRIME-LA D ICI. Qui la detient fabrique des licences.' + '' + '--- a coller dans PM.Licence.pas ---' + '' + 'const' + ' LicencePublicKeyBlob: array[0..71] of Byte = (' + for ($i = 0; $i -lt $blob.Length; $i += 12) { + $row = ($blob[$i..([Math]::Min($i + 11, $blob.Length - 1))] | + ForEach-Object { '$' + $_.ToString('X2') }) -join ', ' + if ($i + 12 -lt $blob.Length) { ' ' + $row + ',' } else { ' ' + $row } + } + ' );' + '' + return +} + +if (-not $For) { throw 'Donne -For (ou -Init pour creer la paire de cles).' } +if (-not (Test-Path $KeyFile)) { throw "Pas de cle privee dans $KeyFile. Lance -Init, ou remets la cle mise a l'abri." } + +$hex = (Get-Content $KeyFile -Raw).Trim() +$priv = [byte[]]::new($hex.Length / 2) +for ($i = 0; $i -lt $priv.Length; $i++) { $priv[$i] = [Convert]::ToByte($hex.Substring($i * 2, 2), 16) } + +$p = New-Object System.Security.Cryptography.ECParameters +$p.Curve = [System.Security.Cryptography.ECCurve]::CreateFromFriendlyName('nistP256') +$p.D = $priv[0..31] +$q = New-Object System.Security.Cryptography.ECPoint +$q.X = $priv[32..63] +$q.Y = $priv[64..95] +$p.Q = $q + +$ecdsa = [System.Security.Cryptography.ECDsa]::Create($p) + +# Charge utile courte : elle voyage dans la cle que l'utilisateur recopie. +# Pas d'expiration - une date qu'on fait respecter avec l'horloge de la +# machine du client ne prouve rien. A ajouter le jour ou on vend un abonnement. +$payload = [ordered]@{ n = $Name; e = $For; d = (Get-Date).ToString('yyyy-MM-dd') } +$json = ($payload | ConvertTo-Json -Compress) +$bytes = [Text.Encoding]::UTF8.GetBytes($json) +$sig = $ecdsa.SignData($bytes, [System.Security.Cryptography.HashAlgorithmName]::SHA256) + +$licence = (ToB64Url $bytes) + '.' + (ToB64Url $sig) + +'' +'Licence pour ' + $For +'' +$licence +'' +'Verification locale : ' + $ecdsa.VerifyData($bytes, $sig, [System.Security.Cryptography.HashAlgorithmName]::SHA256) +''