release.ps1: signing key backup creates the folder, asks the password twice, verifies; -ImportSigningKey to restore
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+55
-5
@@ -10,7 +10,8 @@
|
|||||||
# Signatur: jede Datei wird mit dem privaten Release-Schlüssel signiert (RSA-3072, SHA-256) - der Launcher und das Setup
|
# Signatur: jede Datei wird mit dem privaten Release-Schlüssel signiert (RSA-3072, SHA-256) - der Launcher und das Setup
|
||||||
# installieren nur, was zu ihrem eingebauten öffentlichen Schlüssel passt. Selbst wer den Keymaster oder den Token
|
# installieren nur, was zu ihrem eingebauten öffentlichen Schlüssel passt. Selbst wer den Keymaster oder den Token
|
||||||
# übernimmt, kann so keine fremde Datei als MSCO-Update verteilen. Der Schlüssel liegt DPAPI-verschlüsselt in
|
# übernimmt, kann so keine fremde Datei als MSCO-Update verteilen. Der Schlüssel liegt DPAPI-verschlüsselt in
|
||||||
# %USERPROFILE%\.msco\release-signing-key.dpapi (einmal anlegen: "release.ps1 -NewSigningKey"; SICHERN: "-ExportSigningKey").
|
# %USERPROFILE%\.msco\release-signing-key.dpapi (einmal anlegen: "release.ps1 -NewSigningKey"; SICHERN: "-ExportSigningKey <datei>",
|
||||||
|
# zurückspielen auf einem neuen PC: "-ImportSigningKey <datei>").
|
||||||
# Gibt es die Version auf dem Keymaster schon, passiert nichts - also einfach die Version erhöhen und bauen.
|
# Gibt es die Version auf dem Keymaster schon, passiert nichts - also einfach die Version erhöhen und bauen.
|
||||||
param(
|
param(
|
||||||
[ValidateSet("mod", "launcher", "installer")][string]$Product,
|
[ValidateSet("mod", "launcher", "installer")][string]$Product,
|
||||||
@@ -18,6 +19,7 @@ param(
|
|||||||
[switch]$SaveToken,
|
[switch]$SaveToken,
|
||||||
[switch]$NewSigningKey,
|
[switch]$NewSigningKey,
|
||||||
[string]$ExportSigningKey = "",
|
[string]$ExportSigningKey = "",
|
||||||
|
[string]$ImportSigningKey = "",
|
||||||
[string]$Version = "",
|
[string]$Version = "",
|
||||||
[string]$Changelog = ""
|
[string]$Changelog = ""
|
||||||
)
|
)
|
||||||
@@ -54,6 +56,12 @@ if ($SaveToken) {
|
|||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
$keyEntropy = [Text.Encoding]::UTF8.GetBytes("MSCO release signing key v1")
|
$keyEntropy = [Text.Encoding]::UTF8.GetBytes("MSCO release signing key v1")
|
||||||
|
# Passwort für Backups: verdeckt fragen - oder aus MSCO_BACKUP_PASSWORD (für automatische Backups)
|
||||||
|
function Get-BackupPassword([string]$prompt) {
|
||||||
|
if ($env:MSCO_BACKUP_PASSWORD) { return $env:MSCO_BACKUP_PASSWORD }
|
||||||
|
$secure = Read-Host $prompt -AsSecureString
|
||||||
|
return [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure))
|
||||||
|
}
|
||||||
$keyFile = Find-Secret "release-signing-key.dpapi"
|
$keyFile = Find-Secret "release-signing-key.dpapi"
|
||||||
|
|
||||||
# Neues Schlüsselpaar (nur einmal - der öffentliche Schlüssel ist in Launcher und Setup eingebaut)
|
# Neues Schlüsselpaar (nur einmal - der öffentliche Schlüssel ist in Launcher und Setup eingebaut)
|
||||||
@@ -72,20 +80,62 @@ if ($NewSigningKey) {
|
|||||||
if ($ExportSigningKey) {
|
if ($ExportSigningKey) {
|
||||||
if (-not (Test-Path $keyFile)) { Write-Host "No signing key"; exit 1 }
|
if (-not (Test-Path $keyFile)) { Write-Host "No signing key"; exit 1 }
|
||||||
$private = [Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($keyFile), $keyEntropy, $scope)
|
$private = [Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($keyFile), $keyEntropy, $scope)
|
||||||
$pw = Read-Host "Password for the backup" -AsSecureString
|
$plainPw = Get-BackupPassword "Password for the backup"
|
||||||
$plainPw = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pw))
|
|
||||||
if ($plainPw.Length -lt 12) { Write-Host "Use at least 12 characters"; exit 1 }
|
if ($plainPw.Length -lt 12) { Write-Host "Use at least 12 characters"; exit 1 }
|
||||||
|
# Zweimal fragen: ein Tippfehler machte das Backup sonst unbrauchbar
|
||||||
|
$plainPw2 = Get-BackupPassword "Repeat the password"
|
||||||
|
if ($plainPw2 -ne $plainPw) { Write-Host "The passwords do not match - nothing written"; exit 1 }
|
||||||
$salt = New-Object byte[] 16; (New-Object Security.Cryptography.RNGCryptoServiceProvider).GetBytes($salt)
|
$salt = New-Object byte[] 16; (New-Object Security.Cryptography.RNGCryptoServiceProvider).GetBytes($salt)
|
||||||
$kdf = New-Object Security.Cryptography.Rfc2898DeriveBytes($plainPw, $salt, 300000, [Security.Cryptography.HashAlgorithmName]::SHA256)
|
$kdf = New-Object Security.Cryptography.Rfc2898DeriveBytes($plainPw, $salt, 300000, [Security.Cryptography.HashAlgorithmName]::SHA256)
|
||||||
$aes = [Security.Cryptography.Aes]::Create(); $aes.Key = $kdf.GetBytes(32); $aes.GenerateIV()
|
$aes = [Security.Cryptography.Aes]::Create(); $aes.Key = $kdf.GetBytes(32); $aes.GenerateIV()
|
||||||
$enc = $aes.CreateEncryptor().TransformFinalBlock($private, 0, $private.Length)
|
$enc = $aes.CreateEncryptor().TransformFinalBlock($private, 0, $private.Length)
|
||||||
$out = "MSCO-SIGNING-KEY-BACKUP v1`n" + [Convert]::ToBase64String($salt) + "`n" + [Convert]::ToBase64String($aes.IV) + "`n" + [Convert]::ToBase64String($enc)
|
$out = "MSCO-SIGNING-KEY-BACKUP v1`n" + [Convert]::ToBase64String($salt) + "`n" + [Convert]::ToBase64String($aes.IV) + "`n" + [Convert]::ToBase64String($enc)
|
||||||
|
try {
|
||||||
|
$dir = Split-Path -Parent $ExportSigningKey
|
||||||
|
if ($dir -and -not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
|
||||||
[IO.File]::WriteAllText($ExportSigningKey, $out)
|
[IO.File]::WriteAllText($ExportSigningKey, $out)
|
||||||
Write-Host "Backup written to $ExportSigningKey (AES-256, PBKDF2-SHA256 300000) - keep it and the password safe"
|
} catch {
|
||||||
|
Write-Host "Could not write $ExportSigningKey : $($_.Exception.Message)"
|
||||||
|
Write-Host "Network drive? Drive letters like Z: are not visible in a PowerShell window that runs as administrator."
|
||||||
|
Write-Host "Use a normal (not elevated) PowerShell window, or the network path, e.g. \\server\share\folder\msco-signing-key-backup.txt"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
# Gegenprobe: Datei lesen und mit dem Passwort wieder entschlüsseln
|
||||||
|
$lines = [IO.File]::ReadAllText($ExportSigningKey).Split("`n")
|
||||||
|
$kdf2 = New-Object Security.Cryptography.Rfc2898DeriveBytes($plainPw, [Convert]::FromBase64String($lines[1]), 300000, [Security.Cryptography.HashAlgorithmName]::SHA256)
|
||||||
|
$aes2 = [Security.Cryptography.Aes]::Create(); $aes2.Key = $kdf2.GetBytes(32); $aes2.IV = [Convert]::FromBase64String($lines[2])
|
||||||
|
$check = [Convert]::FromBase64String($lines[3]); $check = $aes2.CreateDecryptor().TransformFinalBlock($check, 0, $check.Length)
|
||||||
|
if ([Convert]::ToBase64String($check) -ne [Convert]::ToBase64String($private)) { Write-Host "Verification FAILED - the backup is not usable"; exit 1 }
|
||||||
|
Write-Host "Backup written and verified: $ExportSigningKey (AES-256, PBKDF2-SHA256 300000) - keep it and the password safe"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
# Sicherung zurückspielen (neuer PC / Windows neu): Datei + Passwort -> wieder DPAPI-verschlüsselt in %USERPROFILE%\.msco
|
||||||
|
if ($ImportSigningKey) {
|
||||||
|
if (-not (Test-Path -LiteralPath $ImportSigningKey)) { Write-Host "Not found: $ImportSigningKey (network drive? see -ExportSigningKey)"; exit 1 }
|
||||||
|
$lines = [IO.File]::ReadAllText($ImportSigningKey).Split("`n")
|
||||||
|
if ($lines.Count -lt 4 -or $lines[0].Trim() -ne "MSCO-SIGNING-KEY-BACKUP v1") { Write-Host "This is not an MSCO signing key backup"; exit 1 }
|
||||||
|
$plainPw = Get-BackupPassword "Password of the backup"
|
||||||
|
try {
|
||||||
|
$kdf = New-Object Security.Cryptography.Rfc2898DeriveBytes($plainPw, [Convert]::FromBase64String($lines[1]), 300000, [Security.Cryptography.HashAlgorithmName]::SHA256)
|
||||||
|
$aes = [Security.Cryptography.Aes]::Create(); $aes.Key = $kdf.GetBytes(32); $aes.IV = [Convert]::FromBase64String($lines[2])
|
||||||
|
$enc = [Convert]::FromBase64String($lines[3])
|
||||||
|
$private = $aes.CreateDecryptor().TransformFinalBlock($enc, 0, $enc.Length)
|
||||||
|
$rsa = New-Object System.Security.Cryptography.RSACng
|
||||||
|
$rsa.FromXmlString([Text.Encoding]::UTF8.GetString($private))
|
||||||
|
} catch { Write-Host "Wrong password or damaged backup"; exit 1 }
|
||||||
|
$target = Join-Path $secretDir "release-signing-key.dpapi"
|
||||||
|
if (Test-Path -LiteralPath $target) {
|
||||||
|
$current = [Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($target), $keyEntropy, $scope)
|
||||||
|
if ([Convert]::ToBase64String($current) -eq [Convert]::ToBase64String($private)) { Write-Host "This key is already installed"; exit 0 }
|
||||||
|
Write-Host "There already is a different signing key in $target - not replaced"; exit 1
|
||||||
|
}
|
||||||
|
New-Item -ItemType Directory -Force -Path $secretDir | Out-Null
|
||||||
|
[IO.File]::WriteAllBytes($target, [Security.Cryptography.ProtectedData]::Protect($private, $keyEntropy, $scope))
|
||||||
|
Write-Host "Signing key restored to $target"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not $Product -or -not $File) { Write-Host "Usage: release.ps1 -Product mod|launcher|installer -File <file> | -SaveToken | -NewSigningKey | -ExportSigningKey <file>"; exit 1 }
|
if (-not $Product -or -not $File) { Write-Host "Usage: release.ps1 -Product mod|launcher|installer -File <file> | -SaveToken | -NewSigningKey | -ExportSigningKey <file> | -ImportSigningKey <file>"; exit 1 }
|
||||||
|
|
||||||
if ($env:MSCO_NO_RELEASE -eq "1") { Write-Host "MSCO release: MSCO_NO_RELEASE=1 - not uploading."; exit 0 }
|
if ($env:MSCO_NO_RELEASE -eq "1") { Write-Host "MSCO release: MSCO_NO_RELEASE=1 - not uploading."; exit 0 }
|
||||||
$token = $env:MSCO_RELEASE_TOKEN
|
$token = $env:MSCO_RELEASE_TOKEN
|
||||||
|
|||||||
Reference in New Issue
Block a user