Here is my update to Dan's excellent code. It handles some special XML characters. Most of the changes have to do with handling nested groups.
Comments are welcome.
#!/usr/bin/perl
# Original author: Dan Buckwalter
#
dbuckwalter@gmail.com
#
# Update by: Bob Swarner July 29, 2008
#
bobswarner@gmail.com
# Updated to account for nested groups and XML characters in the text
# Also, prettyize the XML output
# Trying to elegantly handle the nesting ended up greatly complicating Dan's fine code
#
# Feel free to use/modify this in any way shape or form you wish.
#
# Export your PasswordSafe database into plain text format the use this script to
# convert it into a KeepPassX XML database
#
# Usage:
# perl passwordsafe2keepassx.pl <passwordsafe_export >keepass_import.xml
use warnings;
use strict;
my (%DATA);
sub get_entry_ref( $ $ @ );
sub write_group( $ $ );
my ($TAB) = " ";
#---- clean_text ---------------------------------------------------------------------------
# clean up text data to make it XML'ized
sub clean_text($) {
my ($data) = @_;
$data =~ s/ª/./g;
$data =~ s/&/&/g;
$data =~ s/>/>/g;
$data =~ s/</</g;
return $data;
}
#---- clean_time ---------------------------------------------------------------------------
# clean up time data
sub clean_time($) {
my ($data) = @_;
$data =~ s|/|-|g;
return $data;
}
#---- get_entry_ref -----------------------------------------------------------------
# Given a group array and a key, create a hash to hold the entry
# Call recursively to handle sub-groups
# This is the most complicated bit, but it makes outputing the data a breeze
sub get_entry_ref( $ $ @ ) {
my ($key, $data_ref, @groups) = @_;
my ($group, $count, $ret);
# find how many groups in the array
$count = @groups;
# capture the current group and strip from the @groups array
$group = shift @groups;
if ($count == 0) {
# since we have used up all of our groups, create a hash for the entry and return the reference
$data_ref->{KEYS}{$key}{icon} = "0";
$ret = \%{$data_ref->{KEYS}{$key}};
} else {
# since we still have sub-group(s), create a hash for the group and call recursively to the sub-group(s)
# The {x}=x is just to ensure the hash is created - I imagine there is a better way to do it.
$data_ref->{GROUPS}{$group}{x} = "x";
$ret = get_entry_ref( $key, \%{$data_ref->{GROUPS}{$group}}, @groups );
}
return $ret;
}
#---- read_pwsafe --------------------------------------------------------------------
# Read Password Safe tab-delimited file from standard input and save to memory
sub read_pwsafe() {
my (@list, @groups, $entry, $username, $key, $entry_ref, $comment);
my $is_first = 1;
while (<>) {
# skip the first line with column headings
if ($is_first) {
$is_first = 0;
next;
}
# split the line and strip quotes if present
chop while (/[\n\r]$/);
@list = split /\t/, $_;
# Here is the breakdown of @list (in version 3.13 of Password Safe)
#
# 0: Group/Title
# 1: Username
# 2: Password
# 3: URL
# 4: AutoType
# 5: Created Time
# 6: Password Modified Time
# 7: Last Access Time
# 8: Password Expiry Date
# 9: Password Expiry Interval
# 10: Record Modified Time
# 11: Password Policy
# 12: History
# 13: Notes
# PasswordSafe exports it's entries GroupName[.Groupname].EntryName, so split them into their own vars
# capture as an array to get nested groups
(@groups) = split(/\./, $list[0] );
$entry = pop @groups;
# create an entry in a hash table nested by group and subgroups
$username = clean_text( $list[1] );
$key = "${username} at ${entry}";
$entry_ref = get_entry_ref( $key, \%DATA, @groups );
# save the important bits in the hash reference for this entry, cleaning the text as we go
$entry_ref->{title} = $entry;
$entry_ref->{username} = $username;
$entry_ref->{password} = clean_text( $list[2] );
$entry_ref->{url} = clean_text( $list[3] );
$comment = clean_text( $list[13] );
$comment =~ s/"//g;
$comment =~ s/»/\n/g;
$entry_ref->{comment} = $comment;
$entry_ref->{creation} = clean_time( $list[5] );
$entry_ref->{lastaccess} = clean_time( $list[7] );
$entry_ref->{lastmod} = clean_time( $list[10] );
$entry_ref->{expire} = ($list[8] ? clean_time($list[8]) : "Never" );
}
}
#---- write_entry --------------------------------------------------------------------
# writes the data associated with a particular entry in XML padded by group depth
sub write_entry( $ $ ) {
my ($data_ref,$depth) = @_;
my ($pad) = $TAB x $depth;
my ($padplus) = $TAB x ($depth+1);
my ($key);
print "$pad<entry>\n";
# for each saved key, output in XML format
foreach $key (sort keys %$data_ref ) {
print "$padplus<$key>$data_ref->{$key}</$key>\n";
}
print "$pad</entry>\n";
}
#---- write_group --------------------------------------------------------------------
# writes entries (if any) and sub-groups (if any) for a particular group
# -- call recursively to handle the sub-groups
sub write_group( $ $ ) {
my ($data_ref,$depth) = @_;
my ($group,$key,$entry_ref);
my ($pad) = $TAB x $depth;
my ($padplus) = $TAB x ($depth+1);
# for each entry, call write_entry and pass the hash reference to the entry data
foreach $key (sort keys %{$data_ref->{KEYS}}) {
write_entry( \%{$data_ref->{KEYS}{$key}}, $depth );
}
# for each group, print the group info, then call write_group and pass the hash reference to the group data
foreach $group (sort keys %{$data_ref->{GROUPS}}) {
print "$pad<group>\n";
print "$padplus<title>$group</title>\n";
print "$padplus<icon>0</icon>\n";
write_group( \%{$data_ref->{GROUPS}{$group}}, $depth+1 );
print "$pad</group>\n";
}
}
#---- write_keepassx -----------------------------------------------------------------
# write the main XML wrapper and call write_group for the base group hash
sub write_keepassx() {
my $group;
print "<!DOCTYPE KEEPASSX_DATABASE>\n<database>\n";
write_group( \%DATA, 1 );
print "</database>\n";
}
#==== main ============================================================================
# read the data from standard in
read_pwsafe();
# write the XML to standard out
write_keepassx();
exit(0);