From fe08fa521319851b127319e79740236d012af1f6 Mon Sep 17 00:00:00 2001 From: MrMaho Date: Sun, 4 Oct 2026 21:22:04 +0200 Subject: [PATCH] Release from Windows or Mac - MSCO.csproj is SDK-style now (net35 via Microsoft.NETFramework.ReferenceAssemblies): builds with "dotnet build -c Release" on Windows and macOS; game DLL folder via MSC_MANAGED / -p:MscManaged (default: the game folder). Output verified identical in structure (references, types, methods, resources) to the old project. - release.ps1 works with Windows PowerShell 5.1 and PowerShell 7: secrets in DPAPI files on Windows, in the macOS keychain on a Mac (file with chmod 600 on Linux); cross-platform RSA/password input; release target calls pwsh on non-Windows - RELEASING.md: how to release and how to set up the Mac Co-Authored-By: Claude Opus 5.5 --- MSCO.csproj | 148 +++++++++++++-------------------- RELEASING.md | 66 +++++++++++++++ release.ps1 | 225 +++++++++++++++++++++++++++++++++------------------ 3 files changed, 267 insertions(+), 172 deletions(-) create mode 100644 RELEASING.md diff --git a/MSCO.csproj b/MSCO.csproj index b162b37..418956e 100644 --- a/MSCO.csproj +++ b/MSCO.csproj @@ -1,61 +1,74 @@ - - - + + + - Release - AnyCPU - {CE3FFBAB-946E-9569-CCAA-9CFA2D5C11F3} + net35 Library - Properties - MSCO MSCO - v3.5 - 512 - Unity Full v3.5 + MSCO 13 + disable + disable + + false + Release + + false + false + none + true + false + false + $(NoWarn);CS1591;NU1702 + + + F:\SteamLibrary\steamapps\common\My Summer Car + $(MSC_MANAGED) + $(GamePath)\mysummercar_Data\Managed - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - none - true - bin\Release\ - TRACE - prompt - 4 - + + + + + + + + + + + + + - - F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\UnityEngine.dll + $(MscManaged)\UnityEngine.dll False - F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\Assembly-CSharp-firstpass.dll + $(MscManaged)\Assembly-CSharp-firstpass.dll False - F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\Assembly-CSharp.dll + $(MscManaged)\Assembly-CSharp.dll False - F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\cInput.dll + $(MscManaged)\cInput.dll False - F:\SteamLibrary\steamapps\common\My Summer Car\mysummercar_Data\Managed\PlayMaker.dll + $(MscManaged)\PlayMaker.dll False @@ -63,75 +76,24 @@ False + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - MSCO.menu-logo.png MSCO.logo.png - - - - - + + - + + - + + - \ No newline at end of file + diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..36a01d2 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,66 @@ +# Updates veröffentlichen (Mod & Launcher) + +Neue Versionen der Mod und des Launchers landen beim Bauen automatisch auf dem Keymaster (`release.ps1`). Jede Datei +wird dabei mit dem privaten Release-Schlüssel **signiert** – der Launcher und das Setup installieren nur signierte +Dateien. Dafür braucht der Rechner, auf dem gebaut wird, zwei Geheimnisse: + +| | Windows | macOS | +|---|---|---| +| Release-Token (Keymaster `RELEASE_TOKEN`) | `%USERPROFILE%\.msco\release-token.dpapi` (DPAPI) | Schlüsselbund: *MSCO release-token* | +| Signier-Schlüssel (RSA-3072) | `%USERPROFILE%\.msco\release-signing-key.dpapi` (DPAPI) | Schlüsselbund: *MSCO release-signing-key* | + +Sicherung des Schlüssels: `\\mbmnas\mrmaho\MSCOModBackupData\msco-signing-key-backup.txt` (AES-256, eigenes +Passwort – das Passwort **nicht** auf dem NAS speichern). + +## Ablauf + +1. Version erhöhen: Mod in `MSCO.cs` (`ModVersion`), Launcher in `MSCO.Launcher.csproj` (``) – und den + Abschnitt `## ` in der jeweiligen `CHANGELOG.md` (wird zum Changelog auf der Website/im Launcher). +2. Bauen: + - Mod: im Ordner `MSCO` → `dotnet build -c Release` + - Launcher: im Ordner `MSCO.Launcher` → `dotnet publish -c Release` +3. Die Ausgabe zeigt `MSCO release: mod 2.x.y uploaded to https://msco.mbm.wtf`. Gibt es die Version schon, passiert + nichts. `MSCO_NO_RELEASE=1` (Umgebungsvariable) baut, ohne hochzuladen. +4. Code committen und pushen. + +## Einrichten auf Windows + +Läuft schon (Token und Schlüssel liegen in `%USERPROFILE%\.msco`). Neuer PC: + +```powershell +powershell -ExecutionPolicy Bypass -File MSCO\release.ps1 -SaveToken +powershell -ExecutionPolicy Bypass -File MSCO\release.ps1 -ImportSigningKey \\mbmnas\mrmaho\MSCOModBackupData\msco-signing-key-backup.txt +``` + +> In einem PowerShell-Fenster **ohne** „Als Administrator ausführen“ – sonst sind Netzlaufwerke wie `Z:` nicht da +> (der Pfad `\\mbmnas\mrmaho\…` geht in beiden). + +## Einrichten auf dem Mac (nur bauen/veröffentlichen – das Spiel gibt es nur für Windows) + +1. Werkzeuge: [.NET 10 SDK](https://dotnet.microsoft.com/download) und PowerShell 7: + ```bash + brew install --cask dotnet-sdk powershell + ``` +2. Repositories klonen (`MSCO`, `MSCO-Launcher` von git.mbm.wtf). +3. NAS verbinden (Finder → *Gehe zu* → *Mit Server verbinden* → `smb://mbmnas/mrmaho`) – erscheint unter + `/Volumes/mrmaho`. +4. Spiel-DLLs zum Kompilieren der Mod (UnityEngine, Assembly-CSharp, PlayMaker, cInput) liegen unter + `MSCOModBackupData/Managed` auf dem NAS. Am besten einmal lokal kopieren und den Ordner merken: + ```bash + mkdir -p ~/MSC-Managed && cp /Volumes/mrmaho/MSCOModBackupData/Managed/*.dll ~/MSC-Managed/ + echo 'export MSC_MANAGED="$HOME/MSC-Managed"' >> ~/.zshrc && source ~/.zshrc + ``` +5. Token und Schlüssel in den Schlüsselbund: + ```bash + pwsh MSCO/release.ps1 -SaveToken + pwsh MSCO/release.ps1 -ImportSigningKey /Volumes/mrmaho/MSCOModBackupData/msco-signing-key-backup.txt + ``` +6. Danach wie oben: `dotnet build -c Release` (Mod) bzw. `dotnet publish -c Release` (Launcher). + +Nach einem Spiel-Update (neue `Assembly-CSharp.dll` usw.) die DLLs auf dem NAS bzw. in `~/MSC-Managed` erneuern. + +## Neuer Schlüssel? + +Nur im Notfall (Schlüssel verloren): `release.ps1 -NewSigningKey` erzeugt ein neues Paar – dann muss der neue +öffentliche Schlüssel in `MSCO.Launcher/Services/ReleaseSignature.cs` und `MSCO.Launcher/Setup/Install.cs`, und +Spieler mit einem älteren Launcher müssen ihn einmal neu installieren (ihr Launcher lehnt die neuen Signaturen ab). diff --git a/release.ps1 b/release.ps1 index 7f6ad00..9de9cb0 100644 --- a/release.ps1 +++ b/release.ps1 @@ -1,17 +1,19 @@ # Neue Version automatisch auf den Keymaster hochladen (msco.mbm.wtf -> /download, Launcher-Updates). # Läuft nach dem Release-Build von selbst (MSCO.csproj bzw. MSCO.Launcher.csproj) - oder von Hand: -# powershell -ExecutionPolicy Bypass -File release.ps1 -Product mod -File bin\Release\MSCO.dll +# Windows: powershell -ExecutionPolicy Bypass -File release.ps1 -Product mod -File bin\Release\MSCO.dll +# Mac: pwsh release.ps1 -Product mod -File bin/Release/MSCO.dll (PowerShell 7: brew install powershell) # -# Braucht den Release-Token des Keymasters (RELEASE_TOKEN in dessen .env), lokal verschlüsselt in -# %USERPROFILE%\.msco\release-token.dpapi (Windows DPAPI, nur dein Windows-Konto kann ihn lesen - anlegen mit -# "release.ps1 -SaveToken"), sonst als Umgebungsvariable MSCO_RELEASE_TOKEN. Ohne Token wird nichts hochgeladen. -# MSCO_NO_RELEASE=1 schaltet das Hochladen ab (Test-Builds). +# Braucht den Release-Token des Keymasters (RELEASE_TOKEN in dessen .env) und den privaten Signier-Schlüssel. +# Beide liegen verschlüsselt beim Entwickler: +# Windows: %USERPROFILE%\.msco\*.dpapi (Windows DPAPI - nur dein Windows-Konto kann sie lesen) +# macOS: im Schlüsselbund (Keychain, Einträge "MSCO release-token" / "MSCO release-signing-key") +# Einrichten: "-SaveToken" (Token), Schlüssel einmal mit "-NewSigningKey" erzeugen bzw. auf einem weiteren Rechner mit +# "-ImportSigningKey " aus der Sicherung ("-ExportSigningKey ", mit eigenem Passwort) übernehmen. +# MSCO_RELEASE_TOKEN (Umgebungsvariable) ersetzt den gespeicherten Token, MSCO_NO_RELEASE=1 schaltet Hochladen ab. # -# 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 -# ü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 ", -# zurückspielen auf einem neuen PC: "-ImportSigningKey "). +# Signatur: jede Datei wird signiert (RSA-3072, SHA-256, "MSCO-RELEASE|produkt|version|sha256") - der Launcher und +# das Setup 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. # Gibt es die Version auf dem Keymaster schon, passiert nichts - also einfach die Version erhöhen und bauen. param( [ValidateSet("mod", "launcher", "installer")][string]$Product, @@ -26,68 +28,132 @@ param( $ErrorActionPreference = "Stop" $here = Split-Path -Parent $MyInvocation.MyCommand.Path -Add-Type -AssemblyName System.Security -$entropy = [Text.Encoding]::UTF8.GetBytes("MSCO release token v1") -$scope = [Security.Cryptography.DataProtectionScope]::CurrentUser -# Geheimnisse in %USERPROFILE%\.msco - nicht in AppData: das virtualisieren App-Pakete (z.B. die Claude-App), dann sähe -# ein normales Terminal die Dateien nicht. Die alte Ablage in AppData wird noch gelesen. -$secretDir = Join-Path $env:USERPROFILE ".msco" -function Find-Secret([string]$name) { - $new = Join-Path $secretDir $name - if (Test-Path $new) { return $new } - $old = Join-Path $env:APPDATA "MSCO\$name" - if (Test-Path $old) { return $old } +# ---------------------------------------------------------------- Ablage der Geheimnisse (Windows / macOS / Linux) + +$onWindows = ($PSVersionTable.PSEdition -eq "Desktop") -or ($IsWindows -eq $true) +$onMac = ($IsMacOS -eq $true) +$homeDir = if ($env:USERPROFILE) { $env:USERPROFILE } else { $HOME } +# Nicht in AppData: das virtualisieren App-Pakete (z.B. die Claude-App) - ein normales Terminal sähe die Dateien nicht +$secretDir = Join-Path $homeDir ".msco" +if ($onWindows) { Add-Type -AssemblyName System.Security } + +function Get-SecretFile([string]$name) { + $new = Join-Path $secretDir "$name.dpapi" + if (Test-Path -LiteralPath $new) { return $new } + if ($env:APPDATA) { + $old = Join-Path $env:APPDATA "MSCO\$name.dpapi" # frühere Ablage + if (Test-Path -LiteralPath $old) { return $old } + } return $new } -$tokenDpapi = Find-Secret "release-token.dpapi" + +function Get-SecretLocation([string]$name) { + if ($onWindows) { return Get-SecretFile $name } + if ($onMac) { return "macOS Keychain (MSCO $name)" } + return (Join-Path $secretDir $name) +} + +# Gespeichertes Geheimnis lesen (Bytes) - $null, wenn es keins gibt +function Read-Secret([string]$name, [string]$entropyText) { + if ($onWindows) { + $file = Get-SecretFile $name + if (-not (Test-Path -LiteralPath $file)) { return $null } + $entropy = [Text.Encoding]::UTF8.GetBytes($entropyText) + return [Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($file), $entropy, [Security.Cryptography.DataProtectionScope]::CurrentUser) + } + if ($onMac) { + $value = & security find-generic-password -a $env:USER -s "MSCO $name" -w 2>$null + if ($LASTEXITCODE -ne 0 -or -not $value) { return $null } + return [Convert]::FromBase64String(($value | Out-String).Trim()) + } + $file = Join-Path $secretDir $name + if (-not (Test-Path -LiteralPath $file)) { return $null } + return [Convert]::FromBase64String([IO.File]::ReadAllText($file).Trim()) +} + +function Write-Secret([string]$name, [string]$entropyText, [byte[]]$data) { + if ($onWindows) { + New-Item -ItemType Directory -Force -Path $secretDir | Out-Null + $entropy = [Text.Encoding]::UTF8.GetBytes($entropyText) + $file = Join-Path $secretDir "$name.dpapi" + [IO.File]::WriteAllBytes($file, [Security.Cryptography.ProtectedData]::Protect($data, $entropy, [Security.Cryptography.DataProtectionScope]::CurrentUser)) + return + } + if ($onMac) { + # -U: vorhandenen Eintrag ersetzen; der Wert liegt danach verschlüsselt im Anmelde-Schlüsselbund + & security add-generic-password -U -a $env:USER -s "MSCO $name" -l "MSCO $name" -w ([Convert]::ToBase64String($data)) + if ($LASTEXITCODE -ne 0) { throw "Could not store MSCO $name in the keychain" } + return + } + # Linux: Datei nur für den eigenen Benutzer lesbar (kein Schlüsselbund vorausgesetzt) + New-Item -ItemType Directory -Force -Path $secretDir | Out-Null + $file = Join-Path $secretDir $name + [IO.File]::WriteAllText($file, [Convert]::ToBase64String($data)) + & chmod 600 $file +} + +# RSA: Windows PowerShell 5.1 braucht RSACng (SHA-256), PowerShell 7 (auch macOS) nimmt RSA.Create() +function New-Rsa([int]$bits = 0) { + if ($PSVersionTable.PSEdition -eq "Desktop") { + if ($bits -gt 0) { return New-Object System.Security.Cryptography.RSACng $bits } + return New-Object System.Security.Cryptography.RSACng + } + if ($bits -gt 0) { return [System.Security.Cryptography.RSA]::Create($bits) } + return [System.Security.Cryptography.RSA]::Create() +} + +$tokenName = "release-token"; $tokenEntropy = "MSCO release token v1" +$keyName = "release-signing-key"; $keyEntropy = "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 (New-Object System.Net.NetworkCredential('', $secure)).Password +} + +function Get-Pbkdf2Key([string]$password, [byte[]]$salt) { + $kdf = New-Object Security.Cryptography.Rfc2898DeriveBytes($password, $salt, 300000, [Security.Cryptography.HashAlgorithmName]::SHA256) + return $kdf.GetBytes(32) +} + +# ---------------------------------------------------------------- Einrichten / Sichern # Token einmal verschlüsselt ablegen: fragt verdeckt danach (oder nimmt MSCO_RELEASE_TOKEN) if ($SaveToken) { $plain = $env:MSCO_RELEASE_TOKEN if (-not $plain) { $secure = Read-Host "Release token" -AsSecureString - $plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)) + $plain = (New-Object System.Net.NetworkCredential('', $secure)).Password } if (-not $plain -or $plain.Length -lt 24) { Write-Host "Token too short - nothing saved"; exit 1 } - New-Item -ItemType Directory -Force (Split-Path $tokenDpapi) | Out-Null - $enc = [Security.Cryptography.ProtectedData]::Protect([Text.Encoding]::UTF8.GetBytes($plain), $entropy, $scope) - [IO.File]::WriteAllBytes($tokenDpapi, $enc) - Write-Host "Release token saved encrypted in $tokenDpapi" + Write-Secret $tokenName $tokenEntropy ([Text.Encoding]::UTF8.GetBytes($plain)) + Write-Host "Release token saved encrypted: $(Get-SecretLocation $tokenName)" exit 0 } -$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" # Neues Schlüsselpaar (nur einmal - der öffentliche Schlüssel ist in Launcher und Setup eingebaut) if ($NewSigningKey) { - if (Test-Path $keyFile) { Write-Host "There already is a signing key ($keyFile) - not replaced"; exit 1 } - $rsa = New-Object System.Security.Cryptography.RSACng 3072 - $private = $rsa.ToXmlString($true) - New-Item -ItemType Directory -Force (Split-Path $keyFile) | Out-Null - [IO.File]::WriteAllBytes($keyFile, [Security.Cryptography.ProtectedData]::Protect([Text.Encoding]::UTF8.GetBytes($private), $keyEntropy, $scope)) - Write-Host "Signing key saved encrypted in $keyFile" + if ($null -ne (Read-Secret $keyName $keyEntropy)) { Write-Host "There already is a signing key ($(Get-SecretLocation $keyName)) - not replaced"; exit 1 } + $rsa = New-Rsa 3072 + Write-Secret $keyName $keyEntropy ([Text.Encoding]::UTF8.GetBytes($rsa.ToXmlString($true))) + Write-Host "Signing key saved encrypted: $(Get-SecretLocation $keyName)" Write-Host "PUBLIC KEY (put into Launcher/Setup):" Write-Host $rsa.ToXmlString($false) exit 0 } -# Sicherung des privaten Schlüssels mit eigenem Passwort (AES-256, PBKDF2) - z.B. für einen USB-Stick/Passwort-Manager + +# Sicherung des privaten Schlüssels mit eigenem Passwort (AES-256, PBKDF2) - z.B. auf dem NAS / USB-Stick if ($ExportSigningKey) { - if (-not (Test-Path $keyFile)) { Write-Host "No signing key"; exit 1 } - $private = [Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($keyFile), $keyEntropy, $scope) + $private = Read-Secret $keyName $keyEntropy + if ($null -eq $private) { Write-Host "No signing key ($(Get-SecretLocation $keyName))"; exit 1 } $plainPw = Get-BackupPassword "Password for the backup" 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) - $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() + $salt = New-Object byte[] 16; [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($salt) + $aes = [Security.Cryptography.Aes]::Create(); $aes.Key = Get-Pbkdf2Key $plainPw $salt; $aes.GenerateIV() $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) try { @@ -102,51 +168,50 @@ if ($ExportSigningKey) { } # 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]) + $aes2 = [Security.Cryptography.Aes]::Create(); $aes2.Key = Get-Pbkdf2Key $plainPw ([Convert]::FromBase64String($lines[1])); $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 + +# Sicherung übernehmen (zweiter Rechner, z.B. Mac, oder neuer PC): Datei + Passwort -> verschlüsselte Ablage dieses Rechners 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]) + $aes = [Security.Cryptography.Aes]::Create(); $aes.Key = Get-Pbkdf2Key $plainPw ([Convert]::FromBase64String($lines[1])); $aes.IV = [Convert]::FromBase64String($lines[2]) + $enc = [Convert]::FromBase64String($lines[3].Trim()) $private = $aes.CreateDecryptor().TransformFinalBlock($enc, 0, $enc.Length) - $rsa = New-Object System.Security.Cryptography.RSACng + $rsa = New-Rsa $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) + $current = Read-Secret $keyName $keyEntropy + if ($null -ne $current) { 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 + Write-Host "There already is a different signing key ($(Get-SecretLocation $keyName)) - 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" + Write-Secret $keyName $keyEntropy $private + Write-Host "Signing key restored: $(Get-SecretLocation $keyName)" exit 0 } +# ---------------------------------------------------------------- Hochladen + if (-not $Product -or -not $File) { Write-Host "Usage: release.ps1 -Product mod|launcher|installer -File | -SaveToken | -NewSigningKey | -ExportSigningKey | -ImportSigningKey "; exit 1 } if ($env:MSCO_NO_RELEASE -eq "1") { Write-Host "MSCO release: MSCO_NO_RELEASE=1 - not uploading."; exit 0 } $token = $env:MSCO_RELEASE_TOKEN -if (-not $token -and (Test-Path $tokenDpapi)) { - try { $token = [Text.Encoding]::UTF8.GetString([Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($tokenDpapi), $entropy, $scope)) } - catch { Write-Host "MSCO release: $tokenDpapi cannot be decrypted (other Windows user/PC?) - not uploading."; exit 0 } -} -$tokenFile = Join-Path $env:APPDATA "MSCO\release-token.txt" -if (-not $token -and (Test-Path $tokenFile)) { $token = (Get-Content $tokenFile -Raw).Trim() } if (-not $token) { - Write-Host "MSCO release: no release token ($tokenDpapi or MSCO_RELEASE_TOKEN) - not uploading." + try { + $stored = Read-Secret $tokenName $tokenEntropy + if ($null -ne $stored) { $token = [Text.Encoding]::UTF8.GetString($stored) } + } catch { Write-Host "MSCO release: the stored release token cannot be decrypted (other user/PC?) - not uploading."; exit 0 } +} +if (-not $token) { + Write-Host "MSCO release: no release token ($(Get-SecretLocation $tokenName) or MSCO_RELEASE_TOKEN) - set it with 'release.ps1 -SaveToken'. Not uploading." exit 0 } $keymaster = if ($env:MSCO_KEYMASTER) { $env:MSCO_KEYMASTER.TrimEnd("/") } else { "https://msco.mbm.wtf" } @@ -162,6 +227,7 @@ if (-not $Version) { } if (-not $Version) { $Version = ((Get-Item $File).VersionInfo.ProductVersion -split '\+')[0] } } +if (-not $Version) { Write-Host "MSCO release: version unknown - pass -Version"; exit 1 } # Changelog: Abschnitt "## " aus CHANGELOG.md (Text ohne Markdown, gekürzt) if (-not $Changelog) { @@ -186,19 +252,20 @@ if (-not $Changelog) { $bytes = [System.IO.File]::ReadAllBytes((Resolve-Path $File)) $sha = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace("-", "").ToLowerInvariant() $signature = "" -if (Test-Path $keyFile) { - try { - $private = [Text.Encoding]::UTF8.GetString([Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($keyFile), $keyEntropy, $scope)) - $rsa = New-Object System.Security.Cryptography.RSACng - $rsa.FromXmlString($private) - $message = [Text.Encoding]::UTF8.GetBytes("MSCO-RELEASE|$Product|$Version|$sha") - $signature = [Convert]::ToBase64String($rsa.SignData($message, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1)) - $private = $null - } catch { Write-Host "MSCO release: signing failed ($($_.Exception.Message)) - not uploading"; exit 0 } -} else { - Write-Host "MSCO release: no signing key ($keyFile) - the launcher will refuse this release. Not uploading." +try { + $privateBytes = Read-Secret $keyName $keyEntropy +} catch { Write-Host "MSCO release: the signing key cannot be decrypted (other user/PC?) - not uploading"; exit 0 } +if ($null -eq $privateBytes) { + Write-Host "MSCO release: no signing key ($(Get-SecretLocation $keyName)) - import it with 'release.ps1 -ImportSigningKey '. Not uploading." exit 0 } +try { + $rsa = New-Rsa + $rsa.FromXmlString([Text.Encoding]::UTF8.GetString($privateBytes)) + $message = [Text.Encoding]::UTF8.GetBytes("MSCO-RELEASE|$Product|$Version|$sha") + $signature = [Convert]::ToBase64String($rsa.SignData($message, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1)) + $privateBytes = $null +} catch { Write-Host "MSCO release: signing failed ($($_.Exception.Message)) - not uploading"; exit 0 } Add-Type -AssemblyName System.Net.Http $client = New-Object System.Net.Http.HttpClient