Inleiding
Zoals ik in een eerdere blogpost beschreef, maak ik gebruik van kpcli
,
een commandline applicatie die databases met KeePass-wachtwoorden kan beheren.
De tot nu toe ontbrekende functie binnen kpcli
was een mogelijkheid om een
lijst van wachtwoorden te exporteren. Hiermee zou je dan bijvoorbeeld een
papieren back-up kunnen maken.
Ja, er bestaat minimaal één KeePass2-compatibel programma voor GNU/Linux dat deze mogelijkheden wel biedt, maar het is jammer genoeg (nog) niet toegankelijk voor mijn schermlezer. Ik zocht verder en vond geen alternatief dat zowel toegankelijk was, als kon exporteren onder GNU/Linux.
Automatisering
Toen bedacht ik, dat kpcli
op zich alle informatie op het scherm toont, als je
het juiste commando geeft:
kpcli:/> show /Root/Path/To/Entry
Path: /Root/Path/To
Title: Entry
Uname: myuser
Pass: My$upeR$EcReTp@s$W0Rd
URL: https://example.com/login.php
Notes:
kpcli:/>
Er moest toch een manier zijn om de volledige boomstructuur van de database te
doorlopen, en zo de gegevens op het scherm te toveren? Ik moest op de één of
andere manier een interactieve sessie met kpcli
kunnen automatiseren. Dan
restte alleen nog het 'van het scherm plukken' en wegschrijven als bestand.
Expect
Met de juiste zoektermen kwam ik uiteindelijk terecht bij Expect, een uitbreiding van de TCL programmeertaal, die ik enkel kende uit Mastering Regular Expressions van Jeffrey Friedl. Het is een programma dat speciaal bedoeld is, om volgens een script te kunnen reageren en anticiperen op bestaande commandline programma's - precies wat ik zocht!
#!/usr/bin/expect -f
# Een simpel voorbeeld van een Expect-script, met dank aan Wikipedia
# Open an ftp session to a remote server, and wait for a username prompt.
spawn ftp "ftp.example.com"
expect "username:"
# Send the username, and then wait for a password prompt.
send "my_user_id\r"
expect "password:"
# Send the password, and then wait for an ftp prompt.
send "my_password\r"
expect "ftp>"
# Switch to binary mode, and then wait for an ftp prompt.
send "bin\r"
expect "ftp>"
# Turn off prompting.
send "prompt\r"
expect "ftp>"
# Get all the files
send "mget *\r"
expect "ftp>"
# Exit the ftp session, and wait for a special end-of-file character.
send "bye\r"
expect eof
Bovenstaand voorbeeld toont de meest gebruikte Expect-specifieke commando's. In totaal had ik drie dagen nodig om me thuis te voelen in de syntax van TCL. Daarbij was het principe van de export relatief eenvoudig:
- de hoofdmap van de wachtwoorddatabase uitlezen
- voor elk item een
show
-commando uitvoeren - elke submap uitlezen en items daarin verwerken
Script
Ik zal hieronder het script in verkorte versie beschrijven. De volledige versie is als download beschikbaar.
# Functie die een tekenreeks voorbereid om te worden gebruikt in een CSV-bestand
proc escapeCSV { s } {
regsub -all {\"} $s "\"\"" s # verdubbel alle aanhalingstekens
regsub -all {\r\n\s+} $s "\r\n" s # verwijder alle witruimte aan regelbegin
set s [ string trim $s ] # verwijder alle witruimte aan begin/einde
return \"$s\" # zet aanhalingstekens voor en achter
}
# Laat één item zien en schrijf dit weg naar het export-bestand
proc doEntry { itemT e } {
global exportFile
set j [regsub {\s*[0-9]+\.\s*(.*?)(\s{3,}.*)?} $e {\1}]
send "show \"$itemT$j\"\r"
sleep 0.3
# Hier wordt het spannend; deze reguliere expressie plukt alle gegevens
# uit de output van 'show'. Let met name op de ANSI-escape sequence die
# kpcli rondom het wachtwoord gebruikt om rode tekst op rode achtergrond te
# tonen. Dit is daardoor wel te kopiëren, maar niet visueel te zien.
expect -re "Title: (.*?)\r\nUname: (.*?)\r\n Pass: \u001b\\\[31;41m(.*?)\u001b\\\[0m\r\n URL: (.*?)\r\nNotes: (.*)\r\n\r\n.*"
set fTitle [escapeCSV $expect_out(1,string)]
set fUname [escapeCSV $expect_out(2,string)]
set fPass [escapeCSV $expect_out(3,string)]
set fURL [escapeCSV $expect_out(4,string)]
set fNotes [escapeCSV $expect_out(5,string)]
set csv "$fTitle,$fUname,$fPass,$fURL,$fNotes"
puts $exportFile $csv # schrijf de regel weg naar bestandsbuffer
flush $exportFile # schrijf de bestandsbuffer naar het bestand
}
# Main processing
proc doTotal { item } {
if [regexp {/$} $item] then {
send "ls \"$item\"\r"
sleep 0.2
expect {
-re "=== Groups ===\r\n(.*)" {
set itemS [ split $expect_out(1,string) "\r\n" ]
foreach i $itemS {
doTotal "$item$i" # Heerlijk recursief
}
}
-re "=== Entries ===\r\n(.*)" {
set itemS [ split $expect_out(1,string) "\r\n" ]
foreach i $itemS {
doEntry $item $i
}
}
}
} else {
doEntry "" $item
}
return 0
}
# Check number of arguments
# ...
# Chech for file to process
# ...
# Open export-file for writing
set exportFile [open "~/keepass-export.csv" "w"]
puts $exportFile "Title,Uname,Pass,URL,Notes" # CSV-header
flush $exportFile
# Ask user for password to open file
# ...
# Spawn an instance of kpcli
spawn kpcli --kdb [lindex $::argv 0] # Open kpcli met .kdbx-bestand
expect "Please provide the master password: "
send "$pass\r"
sleep 1
expect 'kpcli:/> ' {
# Execute main call to 'ls'
send "ls /\r"
sleep 0.2
expect -re "=== (Groups|Entries) ===\r\n(.*)"
set body $expect_out(2,string)
set itemS [ split $body "\r\n" ]
foreach i $itemS {
doTotal "$i"
}
send "\r"
sleep 0.2
expect 'kpcli:/> '
send "quit\r"
expect eof
}
# Close export-file
close $exportFile
exit
Conclusie
Een programma of -uitbreiding begint vaak als project dat een bepaald probleem of tekortkoming van een bestaande applicatie oplost. Dit script is precies zo: Ik bouwde wat ik nodig had en nergens anders kon vinden. Het was voor mij een uitdaging om in de vorm van TCL en Expect met een nieuwe programmeertaal kennis te maken.
Natuurlijk was het even doorbijten, maar het was de moeite meer dan waard. Ik kan nu met de spreekwoordelijke druk op de knop onder GNU/Linux een KeePass-export maken met enkel en alleen toegankelijke CLI-programma's.