Created
February 17, 2025 03:43
-
-
Save 2ajoyce/ade5f21d4d31f71c540500a843ec3d04 to your computer and use it in GitHub Desktop.
A Powershell script for updating Mullvad on Windows with GPG/PGP key check
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### Configuration (Change These for Other Programs) | |
$ProgramName = "Mullvad VPN" | |
$ExeUrl = "https://mullvad.net/en/download/app/exe/latest" | |
$SigUrl = "https://mullvad.net/en/download/app/exe/latest/signature" | |
$KeyUrl = "https://mullvad.net/media/mullvad-code-signing.asc" | |
$ExecutablePattern = "MullvadVPN-*.exe" | |
$VersionRegex = "MullvadVPN-([\d\.]+)\.exe" | |
$InstallCommand = "mullvad version" | |
$FingerprintEmail = "[email protected]" | |
### Define Paths | |
$DownloadDir = "$env:TEMP\$ProgramName-Update" | |
### Functions | |
function Ensure-DownloadDir { | |
if (!(Test-Path $DownloadDir)) { New-Item -ItemType Directory -Path $DownloadDir | Out-Null } | |
} | |
function Check-UpdateNeeded { | |
Write-Host " Checking installed $ProgramName version" | |
# Split command and argument correctly | |
$commandParts = $InstallCommand -split " " | |
$command = $commandParts[0] | |
$arguments = $commandParts[1..($commandParts.Length - 1)] | |
# Execute the command and capture errors | |
$output = & $command @arguments 2>&1 | |
if (-not $output -or $output.Count -eq 0) { | |
Write-Output " No output received from '$InstallCommand'. Ensure Mullvad is installed and accessible." | |
return $false | |
} | |
# Extract values from output | |
$currentVersion = if ($output[0] -match "Current version\s+:\s+([\d\.]+)") { $matches[1] } else { $null } | |
$isSupported = if ($output[1] -match "Is supported\s+:\s+(.+)") { $matches[1] } else { $null } | |
$suggestedUpgrade = if ($output[2] -match "Suggested upgrade\s+:\s+(.+)") { $matches[1] } else { $null } | |
$latestStableVersion = if ($output[3] -match "Latest stable version\s+:\s+([\d\.]+)") { $matches[1] } else { $null } | |
Write-Host " Current version: $currentVersion" | |
Write-Host " Is supported: $isSupported" | |
Write-Host " Suggested upgrade: $suggestedUpgrade" | |
Write-Host " Latest stable version: $latestStableVersion" | |
# Ensure versions were extracted | |
if (-not $currentVersion -or -not $latestStableVersion) { | |
Write-Host " Could not determine installed or latest version. Check your installation." | |
return $false | |
} | |
# Compare versions properly | |
if ([version]$currentVersion -ge [version]$latestStableVersion) { | |
Write-Host " No update needed." | |
return $false | |
} | |
if ($suggestedUpgrade -and $suggestedUpgrade -ne "none") { | |
Write-Host " Update needed (Suggested upgrade: $suggestedUpgrade)." | |
return $true | |
} | |
if ($isSupported -and $isSupported -ne "true") { | |
Write-Host " Update needed (Version no longer supported)." | |
return $true | |
} | |
Write-Host " Update needed (Unknown reason)." | |
return $true | |
} | |
function Download-Files { | |
Write-Host "`nResolving filename from server..." | |
# Get the redirected URL to extract the correct filename | |
try { | |
Write-Host "ExeUrl: '$ExeUrl'" | |
$response = Invoke-WebRequest -Uri $ExeUrl -MaximumRedirection 0 -ErrorAction SilentlyContinue | |
$redirectUrl = $response.Headers.Location | |
$fileName = Split-Path -Leaf $redirectUrl | |
Write-Host " Resolved filename: $fileName" | |
} catch { | |
Write-Host " Failed to resolve filename." | |
exit 1 | |
} | |
# Define actual download paths | |
$exePath = "$DownloadDir\$fileName" | |
$sigPath = "$exePath.asc" | |
Write-Host "`nDownloading $fileName..." | |
# Download the files | |
try { | |
Invoke-WebRequest -Uri $redirectUrl -OutFile $exePath | |
Invoke-WebRequest -Uri $SigUrl -OutFile $sigPath | |
Write-Host " Download complete." | |
} catch { | |
Write-Host " Failed to download one or more files. Check your internet connection." | |
exit 1 | |
} | |
# Verify that files exist | |
if (-Not (Test-Path $exePath) -or -Not (Test-Path $sigPath)) { | |
Write-Host " Failed to locate downloaded files." | |
exit 1 | |
} | |
# Output paths | |
Write-Output @{ | |
"exe" = $exePath | |
"sig" = $sigPath | |
} | |
} | |
function Get-DownloadedVersion { | |
param ($exePath) | |
if ($exePath -match $VersionRegex) { | |
return $matches[1] | |
} | |
return $null | |
} | |
function Verify-Signature { | |
param ($sigFile, $exeFile) | |
Write-Host "`nVerifying signature..." | |
# Run GPG verification | |
$gpgResult = & gpg --verify "$sigFile" "$exeFile" 2>&1 | |
if ($gpgResult -match "Good signature from") { | |
Write-Host " Signature verification successful!" | |
Write-Output $true | |
return | |
} | |
Write-Host " Signature verification failed!" | |
Write-Host $gpgResult | |
if ($gpgResult -match "no public key") { | |
Write-Host " No public key found. Attempting to import signing key..." | |
$importSuccess = Import-SigningKey | |
if ($importSuccess) { | |
Write-Host " Retrying signature verification..." | |
return Verify-Signature -sigFile $sigFile -exeFile $exeFile | |
} | |
} | |
Write-Output $false | |
} | |
function Import-SigningKey { | |
Write-Host "`nDownloading signing key..." | |
# Define key file path | |
$keyPath = "$DownloadDir\code-signing.asc" | |
# Download the key | |
try { | |
Invoke-WebRequest -Uri $KeyUrl -OutFile $keyPath | |
Write-Host " Signing key downloaded." | |
} catch { | |
Write-Host " Failed to download signing key. Check your internet connection." | |
Write-Output $false | |
return | |
} | |
Write-Host " Importing key..." | |
$importResult = & gpg --import "$keyPath" 2>&1 | |
if ($importResult -match "imported|existing") { | |
Write-Host " Key imported successfully." | |
} else { | |
Write-Host " Key import failed!" | |
Write-Host $importResult | |
Write-Output $false | |
return | |
} | |
Write-Host "`nVerifying key installation..." | |
$fingerprintResult = & gpg --fingerprint $FingerprintEmail 2>&1 | |
Write-Host $fingerprintResult | |
if ($fingerprintResult -notmatch "Key fingerprint") { | |
Write-Host " Could not verify key fingerprint. Key import may have failed." | |
Write-Output $false | |
return | |
} | |
# Prompt user for optional local signing | |
$signConfirm = Read-Host "Do you want to locally sign the key? (y/n)" | |
if ($signConfirm -eq 'y') { | |
Write-Host " Signing the key locally..." | |
& gpg --sign-key $FingerprintEmail | |
Write-Host " Key signed successfully." | |
} else { | |
Write-Host " Key signing skipped." | |
} | |
Write-Output $true | |
} | |
function Install-Program { | |
param ($exeFile) | |
Write-Host "`nPreparing to install $ProgramName..." | |
# Confirm installation | |
$installConfirm = Read-Host "Do you want to install $ProgramName now? (y/n)" | |
if ($installConfirm -ne 'y') { | |
Write-Host " Installation skipped by user." | |
Write-Output $false | |
return | |
} | |
# Start the installer | |
try { | |
Write-Host " Launching the installer..." | |
Start-Process -FilePath $exeFile -Wait | |
Write-Host " Installation complete." | |
Write-Output $true | |
} catch { | |
Write-Host " Installation failed!" | |
Write-Host $_ | |
Write-Output $false | |
} | |
} | |
function Cleanup-TempFiles { | |
Write-Host "`nCleaning up temporary files..." | |
$cleanupAttempts = 3 | |
$success = $false | |
for ($i = 1; $i -le $cleanupAttempts; $i++) { | |
try { | |
Remove-Item -Path $DownloadDir -Recurse -Force -ErrorAction Stop | |
Write-Host " Temporary files removed successfully." | |
$success = $true | |
break | |
} catch { | |
Write-Host " Attempt $i of $cleanupAttempts - Cleanup failed. Folder may still be in use." | |
Start-Sleep -Seconds 3 | |
} | |
} | |
if (-not $success) { | |
Write-Host " Could not delete temporary files automatically." | |
$manualCleanup = Read-Host "Do you want to open the folder for manual cleanup? (y/n)" | |
if ($manualCleanup -eq 'y') { | |
Invoke-Item $DownloadDir | |
} | |
Write-Output $false | |
return | |
} | |
Write-Output $true | |
} | |
function Main { | |
Ensure-DownloadDir | |
# Check if an update is needed | |
Write-Host "Checking if an update is needed..." | |
$updateNeeded = Check-UpdateNeeded | |
Write-Output "Update needed: $updateNeeded" | |
if (-not $updateNeeded) { | |
Pause | |
exit 0 | |
} | |
Write-Host "`nProceeding with update..." | |
# Download the new version | |
$files = Download-Files | |
$exeFile = $files["exe"] | |
$sigFile = $files["sig"] | |
# Verify the signature | |
if (-not (Verify-Signature -sigFile $sigFile -exeFile $exeFile)) { | |
Write-Host "Failed to verify the signature. Update aborted." | |
Pause | |
exit 1 | |
} | |
# Install the program | |
if (Install-Program -exeFile $exeFile) { | |
Cleanup-TempFiles | |
} | |
Pause | |
} | |
Main |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment