Public Access
Read FileZilla site exports, decoding the Base64 password
FileZilla works, every copy of its settings fails: the <Pass> value in an exported sitemanager XML is Base64, not the password. Copied out verbatim it is rejected by the server every time, which is exactly the "works in the GUI, fails in a script" shape we have been chasing. The test can now read the export itself - listing sites by name so the right practice is picked out of the nine - and decodes the password, reporting both lengths so the difference is visible. A master-password protected export (encoding="crypt") cannot be read, and says so. A typed password that is still Base64 is flagged the same way, since pasting it by hand is the other route to the same mistake. Guarded against false positives: it must decode to printable ASCII, so ordinary passwords are not accused.
This commit is contained in:
+153
-35
@@ -83,53 +83,158 @@ try {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Read-Number($question, $min, $max, $default) {
|
||||||
|
while ($true) {
|
||||||
|
Write-Host "$question " -NoNewline -ForegroundColor White
|
||||||
|
Write-Host "[$default]" -NoNewline -ForegroundColor DarkGray
|
||||||
|
Write-Host ": " -NoNewline -ForegroundColor White
|
||||||
|
$a = Read-Host
|
||||||
|
if ([string]::IsNullOrWhiteSpace($a)) { $a = [string]$default }
|
||||||
|
$n = 0
|
||||||
|
if ([int]::TryParse(([string]$a).Trim(), [ref]$n) -and $n -ge $min -and $n -le $max) { return $n }
|
||||||
|
Write-Host " (a number between $min and $max)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# FileZilla keeps site passwords Base64-encoded, so the value sitting in the
|
||||||
|
# exported XML is not the password - it has to be decoded first. Copying it
|
||||||
|
# out verbatim fails against the server every time, which is exactly what a
|
||||||
|
# working FileZilla login plus a failing script looks like.
|
||||||
|
function Import-FileZillaSite {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "In FileZilla: File > Export, tick 'Site Manager entries', save the .xml." -ForegroundColor Gray
|
||||||
|
$path = Read-Answer "Path to the exported .xml" $null
|
||||||
|
$path = $path.Trim('"').Trim()
|
||||||
|
if (-not (Test-Path $path)) {
|
||||||
|
Write-Host " Nothing at that path." -ForegroundColor Red
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$xml = [xml](Get-Content $path -Raw)
|
||||||
|
} catch {
|
||||||
|
Write-Host " That file is not readable XML - $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$servers = @($xml.SelectNodes("//Server"))
|
||||||
|
if ($servers.Count -eq 0) {
|
||||||
|
Write-Host " No sites found in that file." -ForegroundColor Red
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = $servers[0]
|
||||||
|
if ($servers.Count -gt 1) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Sites in that file:" -ForegroundColor White
|
||||||
|
for ($i = 0; $i -lt $servers.Count; $i++) {
|
||||||
|
$n = $servers[$i]
|
||||||
|
Write-Host (" {0}. {1} {2}@{3}:{4}" -f ($i + 1), $n.Name, $n.User, $n.Host, $n.Port)
|
||||||
|
}
|
||||||
|
$node = $servers[(Read-Number "Which one" 1 $servers.Count 1) - 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$pass = $null
|
||||||
|
$passNode = $node.SelectSingleNode("Pass")
|
||||||
|
if ($passNode) {
|
||||||
|
$encoding = $passNode.GetAttribute("encoding")
|
||||||
|
if ($encoding -eq "base64") {
|
||||||
|
try {
|
||||||
|
$pass = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($passNode.InnerText))
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Decoded the password from Base64." -ForegroundColor Green
|
||||||
|
Write-Host " The XML holds $($passNode.InnerText.Length) characters; the real" -ForegroundColor Gray
|
||||||
|
Write-Host " password is $($pass.Length). Using the decoded one." -ForegroundColor Gray
|
||||||
|
} catch {
|
||||||
|
Write-Host " The password in the XML is not valid Base64." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
} elseif ($encoding -eq "crypt") {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " This export is protected by a FileZilla master password," -ForegroundColor Yellow
|
||||||
|
Write-Host " so the password cannot be read out of it. Type it instead." -ForegroundColor Yellow
|
||||||
|
} else {
|
||||||
|
$pass = $passNode.InnerText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# FileZilla's protocol numbering: 1 is SFTP, 0 is plain FTP, and the
|
||||||
|
# TLS variants use higher values. Only used as the menu default.
|
||||||
|
$proto = 1
|
||||||
|
switch ([string]$node.Protocol) {
|
||||||
|
"0" { $proto = 3 }
|
||||||
|
"1" { $proto = 1 }
|
||||||
|
"3" { $proto = 2 }
|
||||||
|
"4" { $proto = 2 }
|
||||||
|
}
|
||||||
|
|
||||||
|
return @{
|
||||||
|
Host = [string]$node.Host
|
||||||
|
Port = [string]$node.Port
|
||||||
|
User = [string]$node.User
|
||||||
|
Password = $pass
|
||||||
|
Proto = $proto
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fromXml = $null
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
$serverHost = Read-Answer "Server" $null
|
Write-Host "Where are the details coming from?" -ForegroundColor White
|
||||||
|
Write-Host " 1. I will type them in"
|
||||||
|
Write-Host " 2. Read them from a FileZilla site export (.xml)"
|
||||||
|
if ((Read-Number "Choose 1-2" 1 2 1) -eq 2) {
|
||||||
|
$fromXml = Import-FileZillaSite
|
||||||
|
if (-not $fromXml) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Falling back to typing them in." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
$serverHost = Read-Answer "Server" $(if ($fromXml) { $fromXml.Host } else { $null })
|
||||||
$serverHost = ($serverHost -replace '^\w+://', '').Split('/')[0].Trim()
|
$serverHost = ($serverHost -replace '^\w+://', '').Split('/')[0].Trim()
|
||||||
|
|
||||||
$protoChoice = 0
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Protocol:" -ForegroundColor White
|
Write-Host "Protocol:" -ForegroundColor White
|
||||||
Write-Host " 1. SFTP"
|
Write-Host " 1. SFTP"
|
||||||
Write-Host " 2. FTPS"
|
Write-Host " 2. FTPS"
|
||||||
Write-Host " 3. FTP"
|
Write-Host " 3. FTP"
|
||||||
while ($protoChoice -lt 1 -or $protoChoice -gt 3) {
|
$protoDefault = if ($fromXml) { $fromXml.Proto } else { 1 }
|
||||||
Write-Host "Choose 1-3 " -NoNewline -ForegroundColor White
|
$protoChoice = Read-Number "Choose 1-3" 1 3 $protoDefault
|
||||||
Write-Host "[1]" -NoNewline -ForegroundColor DarkGray
|
|
||||||
Write-Host ": " -NoNewline -ForegroundColor White
|
|
||||||
$a = Read-Host
|
|
||||||
if ([string]::IsNullOrWhiteSpace($a)) { $a = "1" }
|
|
||||||
[int]::TryParse($a.Trim(), [ref]$protoChoice) | Out-Null
|
|
||||||
}
|
|
||||||
$protocol = @("sftp", "ftps", "ftp")[$protoChoice - 1]
|
$protocol = @("sftp", "ftps", "ftp")[$protoChoice - 1]
|
||||||
$defaultPort = if ($protocol -eq "sftp") { 22 } else { 21 }
|
|
||||||
|
|
||||||
$port = [int](Read-Answer "Port" $defaultPort)
|
$defaultPort = if ($fromXml -and $fromXml.Port) { $fromXml.Port }
|
||||||
$username = Read-Answer "Username" $null
|
elseif ($protocol -eq "sftp") { 22 } else { 21 }
|
||||||
Write-Host ""
|
$port = Read-Number "Port" 1 65535 $defaultPort
|
||||||
Write-Host "Paste the password. It is not echoed and not saved anywhere." -ForegroundColor DarkGray
|
$username = Read-Answer "Username" $(if ($fromXml) { $fromXml.User } else { $null })
|
||||||
Write-Host "Password: " -NoNewline -ForegroundColor White
|
|
||||||
$secure = Read-Host -AsSecureString
|
|
||||||
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
|
||||||
$password = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
|
|
||||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
|
||||||
|
|
||||||
# Nothing echoes while typing, so a slipped paste looks identical to a good
|
if ($fromXml -and $fromXml.Password) {
|
||||||
# one. Typing it twice is the only way to be sure of what actually went in.
|
$password = $fromXml.Password
|
||||||
Write-Host "Type it once more to check: " -NoNewline -ForegroundColor White
|
} else {
|
||||||
$secure2 = Read-Host -AsSecureString
|
|
||||||
$bstr2 = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure2)
|
|
||||||
$password2 = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr2)
|
|
||||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr2)
|
|
||||||
|
|
||||||
if ($password -ne $password2) {
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host " !! The two did not match." -ForegroundColor Red
|
Write-Host "Paste the password. It is not echoed and not saved anywhere." -ForegroundColor DarkGray
|
||||||
Write-Host " First entry: $($password.Length) characters. Second: $($password2.Length)." -ForegroundColor Red
|
Write-Host "Password: " -NoNewline -ForegroundColor White
|
||||||
Write-Host " One of them is wrong, so this test would prove nothing." -ForegroundColor Red
|
$secure = Read-Host -AsSecureString
|
||||||
Write-Host " Start over and paste carefully, or type it by hand." -ForegroundColor Yellow
|
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)
|
||||||
Write-Host ""
|
$password = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
|
||||||
exit 1
|
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
||||||
|
|
||||||
|
# Nothing echoes while typing, so a slipped paste looks identical to a good
|
||||||
|
# one. Typing it twice is the only way to be sure of what actually went in.
|
||||||
|
Write-Host "Type it once more to check: " -NoNewline -ForegroundColor White
|
||||||
|
$secure2 = Read-Host -AsSecureString
|
||||||
|
$bstr2 = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure2)
|
||||||
|
$password2 = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr2)
|
||||||
|
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr2)
|
||||||
|
|
||||||
|
if ($password -ne $password2) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " !! The two did not match." -ForegroundColor Red
|
||||||
|
Write-Host " First entry: $($password.Length) characters. Second: $($password2.Length)." -ForegroundColor Red
|
||||||
|
Write-Host " One of them is wrong, so this test would prove nothing." -ForegroundColor Red
|
||||||
|
Write-Host " Start over and paste carefully, or type it by hand." -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# -- What the credentials actually contain ------------------------------------
|
# -- What the credentials actually contain ------------------------------------
|
||||||
@@ -163,6 +268,19 @@ if ($password -match '[^\x20-\x7E]') {
|
|||||||
Write-Host " !! The password contains a non-standard character." -ForegroundColor Yellow
|
Write-Host " !! The password contains a non-standard character." -ForegroundColor Yellow
|
||||||
$warned = $true
|
$warned = $true
|
||||||
}
|
}
|
||||||
|
# A password copied straight out of a FileZilla export is still Base64.
|
||||||
|
if (-not $fromXml -and $password.Length -ge 8 -and $password -cmatch '^[A-Za-z0-9+/]+={0,2}$' -and ($password.Length % 4) -eq 0) {
|
||||||
|
$decoded = $null
|
||||||
|
try { $decoded = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($password)) } catch { }
|
||||||
|
if ($decoded -and $decoded -match '^[\x20-\x7E]+$') {
|
||||||
|
Write-Host " !! This looks like Base64, not a password." -ForegroundColor Yellow
|
||||||
|
Write-Host " FileZilla stores passwords Base64-encoded in its exported" -ForegroundColor Yellow
|
||||||
|
Write-Host " XML. Decoded, this is $($decoded.Length) characters - that is" -ForegroundColor Yellow
|
||||||
|
Write-Host " probably the real password. Re-run and choose option 2 to" -ForegroundColor Yellow
|
||||||
|
Write-Host " read the export directly, or paste the decoded value." -ForegroundColor Yellow
|
||||||
|
$warned = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
if (-not $warned) {
|
if (-not $warned) {
|
||||||
Write-Host " Nothing unusual about either string." -ForegroundColor Green
|
Write-Host " Nothing unusual about either string." -ForegroundColor Green
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user