Skip to content

Instantly share code, notes, and snippets.

@2ajoyce
Created February 17, 2025 03:43
Show Gist options
  • Save 2ajoyce/ade5f21d4d31f71c540500a843ec3d04 to your computer and use it in GitHub Desktop.
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
### 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