Compare commits

...
2 Commits
Author SHA1 Message Date
blanceandClaude Fable 5 1965e07c7c Stamp VERSION.txt with b34360c
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FNu4zbdNqYpX5jAVcU31GA
2026-08-30 21:03:57 -05:00
blance b34360c917 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.
2026-08-30 21:03:57 -05:00
2 changed files with 154 additions and 36 deletions
+131 -13
View File
@@ -83,29 +83,133 @@ try {
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 ""
$serverHost = Read-Answer "Server" $null
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 "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()
$protoChoice = 0
Write-Host ""
Write-Host "Protocol:" -ForegroundColor White
Write-Host " 1. SFTP"
Write-Host " 2. FTPS"
Write-Host " 3. FTP"
while ($protoChoice -lt 1 -or $protoChoice -gt 3) {
Write-Host "Choose 1-3 " -NoNewline -ForegroundColor White
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
}
$protoDefault = if ($fromXml) { $fromXml.Proto } else { 1 }
$protoChoice = Read-Number "Choose 1-3" 1 3 $protoDefault
$protocol = @("sftp", "ftps", "ftp")[$protoChoice - 1]
$defaultPort = if ($protocol -eq "sftp") { 22 } else { 21 }
$port = [int](Read-Answer "Port" $defaultPort)
$username = Read-Answer "Username" $null
$defaultPort = if ($fromXml -and $fromXml.Port) { $fromXml.Port }
elseif ($protocol -eq "sftp") { 22 } else { 21 }
$port = Read-Number "Port" 1 65535 $defaultPort
$username = Read-Answer "Username" $(if ($fromXml) { $fromXml.User } else { $null })
if ($fromXml -and $fromXml.Password) {
$password = $fromXml.Password
} else {
Write-Host ""
Write-Host "Paste the password. It is not echoed and not saved anywhere." -ForegroundColor DarkGray
Write-Host "Password: " -NoNewline -ForegroundColor White
@@ -131,6 +235,7 @@ if ($password -ne $password2) {
Write-Host ""
exit 1
}
}
# -- What the credentials actually contain ------------------------------------
Write-Host ""
@@ -163,6 +268,19 @@ if ($password -match '[^\x20-\x7E]') {
Write-Host " !! The password contains a non-standard character." -ForegroundColor Yellow
$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) {
Write-Host " Nothing unusual about either string." -ForegroundColor Green
}
+1 -1
View File
@@ -1 +1 @@
1fa89da 2026-08-30 login test reads the auth outcome
b34360c 2026-08-30 FileZilla export import