From 77c51b52c53cc18d0523e658d8f776cb1b2a6486 Mon Sep 17 00:00:00 2001 From: MrMaho Date: Sat, 3 Oct 2026 18:55:04 +0200 Subject: [PATCH] 2.10.7: changelog; release.ps1 signs releases Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 10 +++++++++ MSCO.cs | 2 +- release.ps1 | 63 +++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5c6d10..59292a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 2.10.7 + +**Sicherheit** +- Daten von Servern und Mitspielern werden auf gültige Längen geprüft – eine gefälschte Längenangabe ließ das Spiel + vorher mit „Out of Memory“ abstürzen +- Mitspieler können nicht mehr beliebige Spiel-Zustände bei anderen auslösen (z.B. am eigenen Spieler oder an + Systemen wie Tod/Speichern) – nur noch Klicks auf klickbare Dinge und echte Schraub-/Montage-Schritte +- Uhrzeit/Wetter und Meldungen im Stil von Server-/Admin-Nachrichten („* …“) nur noch vom Admin bzw. Host +- Release ist mit dem MSCO-Release-Schlüssel signiert + ## 2.10.6 - Neuer Konsolenbefehl **oilcheck**: zeigt, woher Öl ausläuft – die Leck-Werte des Motors (Ölwanne, Ölfilter, diff --git a/MSCO.cs b/MSCO.cs index c5232d3..b8e805a 100644 --- a/MSCO.cs +++ b/MSCO.cs @@ -11,7 +11,7 @@ namespace MSCO /// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit /// Servern und Mitspielern derselben ersten zwei Stellen kompatibel. /// - public const string ModVersion = "2.10.6"; + public const string ModVersion = "2.10.7"; /// /// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann /// auch GameServer.ModVersion im Server. diff --git a/release.ps1 b/release.ps1 index cf4c80a..854a3c4 100644 --- a/release.ps1 +++ b/release.ps1 @@ -6,11 +6,18 @@ # %AppData%\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). +# +# 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 +# %AppData%\MSCO\release-signing-key.dpapi (einmal anlegen: "release.ps1 -NewSigningKey"; SICHERN: "-ExportSigningKey"). # 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, [string]$File, [switch]$SaveToken, + [switch]$NewSigningKey, + [string]$ExportSigningKey = "", [string]$Version = "", [string]$Changelog = "" ) @@ -36,7 +43,39 @@ if ($SaveToken) { Write-Host "Release token saved encrypted in $tokenDpapi" exit 0 } -if (-not $Product -or -not $File) { Write-Host "Usage: release.ps1 -Product mod|launcher|installer -File | release.ps1 -SaveToken"; exit 1 } +$keyEntropy = [Text.Encoding]::UTF8.GetBytes("MSCO release signing key v1") +$keyFile = Join-Path $env:APPDATA "MSCO\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" + 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 +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) + $pw = Read-Host "Password for the backup" -AsSecureString + $plainPw = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pw)) + if ($plainPw.Length -lt 12) { Write-Host "Use at least 12 characters"; 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() + $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) + [IO.File]::WriteAllText($ExportSigningKey, $out) + Write-Host "Backup written to $ExportSigningKey (AES-256, PBKDF2-SHA256 300000) - keep it and the password safe" + exit 0 +} + +if (-not $Product -or -not $File) { Write-Host "Usage: release.ps1 -Product mod|launcher|installer -File | -SaveToken | -NewSigningKey | -ExportSigningKey "; 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 @@ -82,6 +121,25 @@ if (-not $Changelog) { } } +# Signieren: "MSCO-RELEASE|||" - so passt die Signatur nur zu genau dieser Datei, diesem +# Produkt und dieser Version (keine alte Version als neue, keine Mod-DLL als Launcher) +$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." + exit 0 +} + Add-Type -AssemblyName System.Net.Http $client = New-Object System.Net.Http.HttpClient $client.Timeout = [TimeSpan]::FromMinutes(10) @@ -92,10 +150,9 @@ $boundary = "----MSCO" + [Guid]::NewGuid().ToString("N") $utf8 = New-Object System.Text.UTF8Encoding($false) $body = New-Object System.IO.MemoryStream function Write-Text([string]$text) { $b = $utf8.GetBytes($text); $body.Write($b, 0, $b.Length) } -foreach ($field in @(@("product", $Product), @("version", $Version), @("changelog", $Changelog))) { +foreach ($field in @(@("product", $Product), @("version", $Version), @("changelog", $Changelog), @("signature", $signature))) { Write-Text "--$boundary`r`nContent-Disposition: form-data; name=`"$($field[0])`"`r`nContent-Type: text/plain; charset=utf-8`r`n`r`n$($field[1])`r`n" } -$bytes = [System.IO.File]::ReadAllBytes((Resolve-Path $File)) Write-Text "--$boundary`r`nContent-Disposition: form-data; name=`"file`"; filename=`"$([System.IO.Path]::GetFileName($File))`"`r`nContent-Type: application/octet-stream`r`n`r`n" $body.Write($bytes, 0, $bytes.Length) Write-Text "`r`n--$boundary--`r`n"