summaryrefslogtreecommitdiff
path: root/contrib/importers
diff options
context:
space:
mode:
authorJason A. Donenfeld <Jason@zx2c4.com>2014-03-22 12:01:52 -0600
committerJason A. Donenfeld <Jason@zx2c4.com>2014-03-22 12:03:12 -0600
commit47fed2c5d47a03fad7b91bfb890eed257e9c1b2d (patch)
tree46666ba401340524ab9c217a062c366f6e5ec818 /contrib/importers
parent414bab7d973b50431854496811608c549fb541e1 (diff)
downloadpass-47fed2c5d47a03fad7b91bfb890eed257e9c1b2d.tar.gz
pass-47fed2c5d47a03fad7b91bfb890eed257e9c1b2d.tar.bz2
pass-47fed2c5d47a03fad7b91bfb890eed257e9c1b2d.zip
Makefile: do not use recursion and organize
Diffstat (limited to 'contrib/importers')
-rwxr-xr-xcontrib/importers/fpm2pass.pl79
-rwxr-xr-xcontrib/importers/gorilla2pass.rb76
-rwxr-xr-xcontrib/importers/kedpm2pass.py52
-rwxr-xr-xcontrib/importers/keepass2pass.py139
-rwxr-xr-xcontrib/importers/keepassx2pass.py76
-rwxr-xr-xcontrib/importers/lastpass2pass.rb131
-rwxr-xr-xcontrib/importers/pwsafe2pass.sh30
-rwxr-xr-xcontrib/importers/revelation2pass.py172
8 files changed, 755 insertions, 0 deletions
diff --git a/contrib/importers/fpm2pass.pl b/contrib/importers/fpm2pass.pl
new file mode 100755
index 0000000..d1a0908
--- /dev/null
+++ b/contrib/importers/fpm2pass.pl
@@ -0,0 +1,79 @@
+#!/usr/bin/perl
+
+# Copyright (C) 2012 Jeffrey Ratcliffe <jeffrey.ratcliffe@gmail.com>. All Rights Reserved.
+# This file is licensed under the GPLv2+. Please see COPYING for more information.
+
+use warnings;
+use strict;
+use XML::Simple;
+use Getopt::Long;
+use Pod::Usage;
+
+my ($help, $man);
+my @args = ('help' => \$help,
+ 'man' => \$man,);
+GetOptions (@args) or pod2usage(2);
+pod2usage(1) if ($help);
+pod2usage(-exitstatus => 0, -verbose => 2) if $man;
+pod2usage(
+ -msg => "Syntax error: must specify a file to read.",
+ -exitval => 2,
+ -verbose => 1
+)
+ if (@ARGV != 1);
+
+# Grab the XML to a perl structure
+my $xs = XML::Simple->new();
+my $doc = $xs->XMLin(shift);
+
+for (@{$doc->{PasswordList}{PasswordItem}}) {
+ my $name;
+ if (ref($_->{category}) eq 'HASH') {
+ $name = escape($_->{title});
+ }
+ else {
+ $name = escape($_->{category})."/".escape($_->{title});
+ }
+ my $contents = '';
+ $contents .= "$_->{password}\n" unless (ref($_->{password}) eq 'HASH');
+ $contents .= "user $_->{user}\n" unless (ref($_->{user}) eq 'HASH');
+ $contents .= "url $_->{url}\n" unless (ref($_->{url}) eq 'HASH');
+ unless (ref($_->{notes}) eq 'HASH') {
+ $_->{notes} =~ s/\n/\n /g;
+ $contents .= "notes:\n $_->{notes}\n";
+ }
+ my $cmd = "pass insert -f -m $name";
+ my $pid = open(my $fh, "| $cmd") or die "Couldn't fork: $!\n";
+ print $fh $contents;
+ close $fh;
+}
+
+# escape inverted commas, spaces, ampersands and brackets
+sub escape {
+ my ($s) = @_;
+ $s =~ s/\//-/g;
+ $s =~ s/(['\(\) &])/\\$1/g;
+ return $s;
+}
+
+=head1 NAME
+
+ fpm2pass.pl - imports an .xml exported by fpm2 into pass
+
+=head1 SYNOPSIS
+
+=head1 USAGE
+
+ fpm2pass.pl [--help] [--man] <xml>
+
+The following options are available:
+
+=over
+
+=item --help
+
+=item --man
+
+=back
+
+=cut
diff --git a/contrib/importers/gorilla2pass.rb b/contrib/importers/gorilla2pass.rb
new file mode 100755
index 0000000..bf168a7
--- /dev/null
+++ b/contrib/importers/gorilla2pass.rb
@@ -0,0 +1,76 @@
+#!/usr/bin/env ruby
+
+# Copyright (C) 2013 David Sklar <david.sklar@gmail.com>. All Rights Reserved.
+# This file is licensed under the GPLv2+. Please see COPYING for more information.
+
+entries = {}
+
+class HashCounter
+
+ def initialize
+ @h = Hash.new {|h,k| h[k] = 2 }
+ end
+
+ def get(k)
+ v = @h[k]
+ @h[k] = v + 1
+ v
+ end
+end
+
+hc = HashCounter.new
+
+$stdin.each do |line|
+ uuid, group, title, url, user, password, notes = line.strip.split(',')
+ next if uuid == "uuid"
+
+ # check for missing group
+ # check for missing title
+
+ prefix = "#{group}/#{title}".gsub(/[\s\'\"()!]/,'')
+
+
+ if user && user.length > 0
+ entries["#{prefix}/user"] = user
+ end
+ if url && url.length > 0
+ entries["#{prefix}/url"] = url
+ end
+ if password && password.length > 0
+ entries["#{prefix}/password"] = password
+ end
+ if notes && notes.length > 0
+ entries["#{prefix}/notes"] = notes.gsub('\n',"\n").strip
+ end
+end
+
+entries.keys.each do |k|
+ if k =~ /^(.+?)-merged\d{4}-\d\d-\d\d\d\d:\d\d:\d\d(\/.+)$/
+ other = $1 + $2
+ if entries.has_key?(other)
+ if entries[k] == entries[other]
+ entries.delete(k)
+ else
+ i = hc.get(other)
+ entries["#{other}#{i}"] = entries[k]
+ entries.delete(k)
+ end
+ else
+ entries[other] = entries[k]
+ entries.delete(k)
+ end
+ end
+end
+
+pass_top_level = "Gorilla"
+entries.keys.each do |k|
+ print "#{k}...(#{entries[k]})..."
+ IO.popen("pass insert -e -f '#{pass_top_level}/#{k}' > /dev/null", 'w') do |io|
+ io.puts entries[k] + "\n"
+ end
+ if $? == 0
+ puts " done!"
+ else
+ puts " error!"
+ end
+end
diff --git a/contrib/importers/kedpm2pass.py b/contrib/importers/kedpm2pass.py
new file mode 100755
index 0000000..b79cc8b
--- /dev/null
+++ b/contrib/importers/kedpm2pass.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012 Antoine Beaupré <anarcat@orangeseeds.org>. All Rights Reserved.
+# This file is licensed under the GPLv2+. Please see COPYING for more information.
+#
+# To double-check your import worked:
+# grep Path passwords | sed 's#^Path: ##;s/$/.gpg/' | sort > listpaths
+# (cd ~/.password-store/ ; find -type f ) | sort | diff -u - listpaths
+
+import re
+import fileinput
+
+import sys # for exit
+
+import subprocess
+
+def insert(d):
+ path = d['Path']
+ del d['Path']
+ print "inserting " + path
+ content = d['Password'] + "\n"
+ del d['Password']
+ for k, v in d.iteritems():
+ content += "%s: %s\n" % (k, v)
+ del d
+ cmd = ["pass", "insert", "--force", "--multiline", path]
+ process = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ stdout, stderr = process.communicate(content)
+ retcode = process.wait()
+ if retcode:
+ print 'command "%s" failed with exit code %d: %s' % (" ".join(cmd), retcode, stdout + stderr)
+ sys.exit(1);
+
+d = None
+for line in fileinput.input():
+ if line == "\n":
+ continue
+ match = re.match("(\w+): (.*)$", line)
+ if match:
+ if match.group(1) == 'Path':
+ if d is not None:
+ insert(d)
+ else:
+ d = {}
+ d[match.group(1)] = match.group(2)
+ #print "found field: %s => %s" % (match.group(1), match.group(2))
+ else:
+ print "warning: no match found on line: *%s*" % line
+
+if d is not None:
+ insert(d)
diff --git a/contrib/importers/keepass2pass.py b/contrib/importers/keepass2pass.py
new file mode 100755
index 0000000..80a2ad9
--- /dev/null
+++ b/contrib/importers/keepass2pass.py
@@ -0,0 +1,139 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013 Stefan Simroth <stefan.simroth@gmail.com>. All Rights Reserved.
+# Based on the script for KeepassX by Juhamatti Niemelä <iiska@iki.fi>.
+# This file is licensed under the GPLv2+. Please see COPYING for more information.
+#
+# Usage:
+# ./keepass2pass.py -f export.xml
+# By default, takes the name of the root element and puts all passwords in there, but you can disable this:
+# ./keepass2pass.py -f export.xml -r ""
+# Or you can use another root folder:
+# ./keepass2pass.py -f export.xml -r foo
+#
+# Features:
+# * This script can handle duplicates and will merge them.
+# * Besides the password also the fields 'UserName', 'URL' and 'Notes' (comment) will be inserted.
+# * You get a warning if an entry has no password, but it will still insert it.
+
+import getopt, sys
+from subprocess import Popen, PIPE
+from xml.etree import ElementTree
+
+
+def pass_import_entry(path, data):
+ """ Import new password entry to password-store using pass insert command """
+ proc = Popen(['pass', 'insert', '--multiline', path], stdin=PIPE, stdout=PIPE)
+ proc.communicate(data.encode('utf8'))
+ proc.wait()
+
+
+def get_value(elements, node_text):
+ for element in elements:
+ for child in element.findall('Key'):
+ if child.text == node_text:
+ return element.find('Value').text
+ return ''
+
+def path_for(element, path=''):
+ """ Generate path name from elements title and current path """
+ if element.tag == 'Entry':
+ title = get_value(element.findall("String"), "Title")
+ elif element.tag == 'Group':
+ title = element.find('Name').text
+ else: title = ''
+
+ if path == '': return title
+ else: return '/'.join([path, title])
+
+def password_data(element, path=''):
+ """ Return password data and additional info if available from password entry element. """
+ data = ""
+ password = get_value(element.findall('String'), 'Password')
+ if password is not None: data = password + "\n"
+ else:
+ print "[WARN] No password: %s" % path_for(element, path)
+
+ for field in ['UserName', 'URL', 'Notes']:
+ value = get_value(element, field)
+ if value is not None and not len(value) == 0:
+ data = "%s%s: %s\n" % (data, field, value)
+ return data
+
+def import_entry(entries, element, path=''):
+ element_path = path_for(element, path)
+ if entries.has_key(element_path):
+ print "[INFO] Duplicate needs merging: %s" % element_path
+ existing_data = entries[element_path]
+ data = "%s---------\nPassword: %s" % (existing_data, password_data(element))
+ else:
+ data = password_data(element, path)
+
+ entries[element_path] = data
+
+def import_group(entries, element, path=''):
+ """ Import all entries and sub-groups from given group """
+ npath = path_for(element, path)
+ for group in element.findall('Group'):
+ import_group(entries, group, npath)
+ for entry in element.findall('Entry'):
+ import_entry(entries, entry, npath)
+
+def import_passwords(xml_file, root_path=None):
+ """ Parse given Keepass2 XML file and import password groups from it """
+ print "[>>>>] Importing passwords from file %s" % xml_file
+ print "[INFO] Root path: %s" % root_path
+ entries = dict()
+ with open(xml_file) as xml:
+ text = xml.read()
+ xml_tree = ElementTree.XML(text)
+ root = xml_tree.find('Root')
+ root_group = root.find('Group')
+ import_group(entries,root_group,'')
+ if root_path is None: root_path = root_group.find('Name').text
+ groups = root_group.findall('Group')
+ for group in groups:
+ import_group(entries, group, root_path)
+ password_count = 0
+ for path, data in sorted(entries.iteritems()):
+ sys.stdout.write("[>>>>] Importing %s ... " % path.encode("utf-8"))
+ pass_import_entry(path, data)
+ sys.stdout.write("OK\n")
+ password_count += 1
+
+ print "[ OK ] Done. Imported %i passwords." % password_count
+
+
+def usage():
+ """ Print usage """
+ print "Usage: %s -f XML_FILE" % (sys.argv[0])
+ print "Optional:"
+ print " -r ROOT_PATH Different root path to use than the one in xml file, use \"\" for none"
+
+
+def main(argv):
+ try:
+ opts, args = getopt.gnu_getopt(argv, "f:r:")
+ except getopt.GetoptError as err:
+ print str(err)
+ usage()
+ sys.exit(2)
+
+ xml_file = None
+ root_path = None
+
+ for opt, arg in opts:
+ if opt in "-f":
+ xml_file = arg
+ if opt in "-r":
+ root_path = arg
+
+ if xml_file is not None:
+ import_passwords(xml_file, root_path)
+ else:
+ usage()
+ sys.exit(2)
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
diff --git a/contrib/importers/keepassx2pass.py b/contrib/importers/keepassx2pass.py
new file mode 100755
index 0000000..dc4b1e5
--- /dev/null
+++ b/contrib/importers/keepassx2pass.py
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2012 Juhamatti Niemelä <iiska@iki.fi>. All Rights Reserved.
+# This file is licensed under the GPLv2+. Please see COPYING for more information.
+
+import sys
+import re
+
+from subprocess import Popen, PIPE
+from xml.etree import ElementTree
+
+def space_to_camelcase(value):
+ output = ""
+ first_word_passed = False
+ for word in value.split(" "):
+ if not word:
+ output += "_"
+ continue
+ if first_word_passed:
+ output += word.capitalize()
+ else:
+ output += word.lower()
+ first_word_passed = True
+ return output
+
+def cleanTitle(title):
+ # make the title more command line friendly
+ title = re.sub("(\\|\||\(|\))", "-", title)
+ title = re.sub("-$", "", title)
+ title = re.sub("\@", "At", title)
+ title = re.sub("'", "", title)
+ return title
+
+def path_for(element, path=''):
+ """ Generate path name from elements title and current path """
+ title = cleanTitle(space_to_camelcase(element.find('title').text))
+ return '/'.join([path, title])
+
+def password_data(element):
+ """ Return password data and additional info if available from
+ password entry element. """
+ passwd = element.find('password').text
+ ret = passwd + "\n" if passwd else "\n"
+ for field in ['username', 'url', 'comment']:
+ fel = element.find(field)
+ if fel.text is not None:
+ ret = "%s%s: %s\n" % (ret, fel.tag, fel.text)
+ return ret
+
+def import_entry(element, path=''):
+ """ Import new password entry to password-store using pass insert
+ command """
+ proc = Popen(['pass', 'insert', '--multiline', '--force',
+ path_for(element, path)],
+ stdin=PIPE, stdout=PIPE)
+ proc.communicate(password_data(element).encode('utf8'))
+ proc.wait()
+
+def import_group(element, path=''):
+ """ Import all entries and sub-groups from given group """
+ npath = path_for(element, path)
+ for group in element.findall('group'):
+ import_group(group, npath)
+ for entry in element.findall('entry'):
+ import_entry(entry, npath)
+
+
+def main(xml_file):
+ """ Parse given KeepassX XML file and import password groups from it """
+ with open(xml_file) as xml:
+ for group in ElementTree.XML(xml.read()).findall('group'):
+ import_group(group)
+
+if __name__ == '__main__':
+ main(sys.argv[1])
diff --git a/contrib/importers/lastpass2pass.rb b/contrib/importers/lastpass2pass.rb
new file mode 100755
index 0000000..41a2a29
--- /dev/null
+++ b/contrib/importers/lastpass2pass.rb
@@ -0,0 +1,131 @@
+#!/usr/bin/env ruby
+
+# Copyright (C) 2012 Alex Sayers <alex.sayers@gmail.com>. All Rights Reserved.
+# This file is licensed under the GPLv2+. Please see COPYING for more information.
+
+# LastPass Importer
+#
+# Reads CSV files exported from LastPass and imports them into pass.
+
+# Usage:
+#
+# Go to lastpass.com and sign in. Next click on your username in the top-right
+# corner. In the drop-down meny that appears, click "Export". After filling in
+# your details again, copy the text and save it somewhere on your disk. Make sure
+# you copy the whole thing, and resist the temptation to "Save Page As" - the
+# script doesn't like HTML.
+#
+# Fire up a terminal and run the script, passing the file you saved as an argument.
+# It should look something like this:
+#
+#$ ./lastpass2pass.rb path/to/passwords_file.csv
+
+# Parse flags
+require 'optparse'
+optparse = OptionParser.new do |opts|
+ opts.banner = "Usage: #{$0} [options] filename"
+
+ FORCE = false
+ opts.on("-f", "--force", "Overwrite existing records") { FORCE = true }
+ DEFAULT_GROUP = ""
+ opts.on("-d", "--default GROUP", "Place uncategorised records into GROUP") { |group| DEFAULT_GROUP = group }
+ opts.on("-h", "--help", "Display this screen") { puts opts; exit }
+
+ opts.parse!
+end
+
+# Check for a filename
+if ARGV.empty?
+ puts optparse
+ exit 0
+end
+
+# Get filename of csv file
+filename = ARGV.join(" ")
+puts "Reading '#{filename}'..."
+
+
+class Record
+ def initialize name, url, username, password, extra, grouping, fav
+ @name, @url, @username, @password, @extra, @grouping, @fav = name, url, username, password, extra, grouping, fav
+ end
+
+ def name
+ s = ""
+ s << @grouping + "/" unless @grouping.empty?
+ s << @name
+ s.gsub(/ /, "_").gsub(/'/, "")
+ end
+
+ def to_s
+ s = ""
+ s << "#{@password}\n---\n"
+ s << "#{@grouping} / " unless @grouping.empty?
+ s << "#{@name}\n"
+ s << "username: #{@username}\n" unless @username.empty?
+ s << "password: #{@password}\n" unless @password.empty?
+ s << "url: #{@url}\n" unless @url == "http://sn"
+ s << "#{@extra}\n" unless @extra.nil?
+ return s
+ end
+end
+
+# Extract individual records
+entries = []
+entry = ""
+begin
+ file = File.open(filename)
+ file.each do |line|
+ if line =~ /^http/
+ entries.push(entry)
+ entry = ""
+ end
+ entry += line
+ end
+ entries.push(entry)
+ entries.shift
+ puts "#{entries.length} records found!"
+rescue
+ puts "Couldn't find #{filename}!"
+ exit 1
+end
+
+# Parse records and create Record objects
+records = []
+entries.each do |e|
+ args = e.split(",")
+ url = args.shift
+ username = args.shift
+ password = args.shift
+ fav = args.pop
+ grouping = args.pop
+ grouping = DEFAULT_GROUP if grouping.empty?
+ name = args.pop
+ extra = args.join(",")[1...-1]
+
+ records << Record.new(name, url, username, password, extra, grouping, fav)
+end
+puts "Records parsed: #{records.length}"
+
+successful = 0
+errors = []
+records.each do |r|
+ print "Creating record #{r.name}..."
+ IO.popen("pass insert -m#{"f" if FORCE} '#{r.name}' > /dev/null", 'w') do |io|
+ io.puts r
+ end
+ if $? == 0
+ puts " done!"
+ successful += 1
+ else
+ puts " error!"
+ errors << r
+ end
+end
+puts "#{successful} records successfully imported!"
+
+if errors
+ puts "There were #{errors.length} errors:"
+ errors.each { |e| print e.name + (e == errors.last ? ".\n" : ", ")}
+ puts "These probably occurred because an identically-named record already existed, or because there were multiple entries with the same name in the csv file."
+end
diff --git a/contrib/importers/pwsafe2pass.sh b/contrib/importers/pwsafe2pass.sh
new file mode 100755
index 0000000..c29bb3f
--- /dev/null
+++ b/contrib/importers/pwsafe2pass.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+# Copyright (C) 2013 Tom Hendrikx <tom@whyscream.net>. All Rights Reserved.
+# This file is licensed under the GPLv2+. Please see COPYING for more information.
+
+export=$1
+
+IFS=" " # tab character
+cat "$export" | while read uuid group name login passwd notes; do
+ test "$uuid" = "# passwordsafe version 2.0 database" && continue
+ test "$uuid" = "uuid" && continue
+ test "$name" = '""' && continue;
+
+ group="$(echo $group | cut -d'"' -f2)"
+ login="$(echo $login | cut -d'"' -f2)"
+ passwd="$(echo $passwd | cut -d'"' -f2)"
+ name="$(echo $name | cut -d'"' -f2)"
+
+ # cleanup
+ test "${name:0:4}" = "http" && name="$(echo $name | cut -d'/' -f3)"
+ test "${name:0:4}" = "www." && name="$(echo $name | cut -c 5-)"
+
+ entry=""
+ test -n "$login" && entry="${entry}login: $login\n"
+ test -n "$passwd" && entry="${entry}pass: $passwd\n"
+ test -n "$group" && entry="${entry}group: $group\n"
+
+ echo Adding entry for $name:
+ echo -e $entry | pass insert --multiline --force "$name"
+ test $? && echo "Added!"
+done
diff --git a/contrib/importers/revelation2pass.py b/contrib/importers/revelation2pass.py
new file mode 100755
index 0000000..f04c1a8
--- /dev/null
+++ b/contrib/importers/revelation2pass.py
@@ -0,0 +1,172 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013 Emanuele Aina <em@nerd.ocracy.org>. All Rights Reserved.
+# Copyright (C) 2011 Toni Corvera. All Rights Reserved.
+# This file is licensed under the BSD 2-clause license:
+# http://www.opensource.org/licenses/BSD-2-Clause
+#
+# Import script for the Revelation password manager:
+# http://revelation.olasagasti.info/
+# Heavily based on the Relevation command line tool:
+# http://p.outlyer.net/relevation/
+
+import os, sys, argparse, zlib, getpass, traceback
+from subprocess import Popen, PIPE, STDOUT, CalledProcessError
+from collections import OrderedDict
+try:
+ from lxml import etree
+except ImportError:
+ from xml.etree import ElementTree as etree
+
+USE_PYCRYPTO = True
+try:
+ from Crypto.Cipher import AES
+except ImportError:
+ USE_PYCRYPTO = False
+ try:
+ from crypto.cipher import rijndael, cbc
+ from crypto.cipher.base import noPadding
+ except ImportError:
+ sys.stderr.write('Either PyCrypto or cryptopy are required\n')
+ raise
+
+def path_for(element, path=None):
+ """ Generate path name from elements name and current path """
+ name = element.find('name').text
+ name = name.replace('/', '-').replace('\\', '-')
+ path = path if path else ''
+ return os.path.join(path, name)
+
+def format_password_data(data):
+ """ Format the secret data that will be handed to Pass in multi-line mode:
+ $password
+ $fieldname: $fielddata
+ ...
+ $multi_line_notes_with_leading_spaces"""
+ password = data.pop('password', None) or ''
+ ret = password + '\n'
+ notes = data.pop('notes', None)
+ for label, text in data.iteritems():
+ ret += label + ': ' + text + '\n'
+ if notes:
+ ret += ' ' + notes.replace('\n', '\n ').strip() + '\n'
+ return ret
+
+def password_data(element):
+ """ Return password data and additional info if available from
+ password entry element. """
+ data = OrderedDict()
+ data['password'] = element.find('field[@id="generic-password"]').text
+ data['type'] = element.attrib['type']
+ for field in element.findall('field'):
+ field_id = field.attrib['id']
+ if field_id == 'generic-password':
+ continue
+ if field.text is not None:
+ data[field_id] = field.text
+ for tag in ('description', 'notes'):
+ field = element.find(tag)
+ if field is not None and field.text:
+ data[tag] = field.text
+ return format_password_data(data)
+
+
+def import_entry(element, path=None, verbose=0):
+ """ Import new password entry to password-store using pass insert
+ command """
+ cmd = ['pass', 'insert', '--multiline', '--force', path_for(element, path)]
+ if verbose:
+ print 'cmd:\n ' + ' '.join(cmd)
+ proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT)
+ stdin = password_data(element).encode('utf8')
+ if verbose:
+ print 'input:\n ' + stdin.replace('\n', '\n ').strip()
+ stdout, _ = proc.communicate(stdin)
+ retcode = proc.poll()
+ if retcode:
+ raise CalledProcessError(retcode, cmd, output=stdout)
+
+def import_folder(element, path=None, verbose=0):
+ path = path_for(element, path)
+ import_subentries(element, path, verbose)
+
+def import_subentries(element, path=None, verbose=0):
+ """ Import all sub entries of the current folder element """
+ for entry in element.findall('entry'):
+ if entry.attrib['type'] == 'folder':
+ import_folder(entry, path, verbose)
+ else:
+ import_entry(entry, path, verbose)
+
+def decrypt_gz(key, cipher_text):
+ ''' Decrypt cipher_text using key.
+ decrypt(str, str) -> cleartext (gzipped xml)
+
+ This function will use the underlying, available, cipher module.
+ '''
+ if USE_PYCRYPTO:
+ # Extract IV
+ c = AES.new(key)
+ iv = c.decrypt(cipher_text[12:28])
+ # Decrypt data, CBC mode
+ c = AES.new(key, AES.MODE_CBC, iv)
+ ct = c.decrypt(cipher_text[28:])
+ else:
+ # Extract IV
+ c = rijndael.Rijndael(key, keySize=len(key), padding=noPadding())
+ iv = c.decrypt(cipher_text[12:28])
+ # Decrypt data, CBC mode
+ bc = rijndael.Rijndael(key, keySize=len(key), padding=noPadding())
+ c = cbc.CBC(bc, padding=noPadding())
+ ct = c.decrypt(cipher_text[28:], iv=iv)
+ return ct
+
+def main(datafile, verbose=False):
+ f = None
+ with open(datafile, "rb") as f:
+ # Encrypted data
+ data = f.read()
+ password = getpass.getpass()
+ # Pad password
+ password += (chr(0) * (32 - len(password)))
+ # Decrypt. Decrypted data is compressed
+ cleardata_gz = decrypt_gz(password, data)
+ # Length of data padding
+ padlen = ord(cleardata_gz[-1])
+ # Decompress actual data (15 is wbits [ref3] DON'T CHANGE, 2**15 is the (initial) buf size)
+ xmldata = zlib.decompress(cleardata_gz[:-padlen], 15, 2**15)
+ root = etree.fromstring(xmldata)
+ import_subentries(root, verbose=verbose)
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--verbose', '-v', action='count')
+ parser.add_argument('FILE', help="the file storing the Revelation passwords")
+ args = parser.parse_args()
+
+ def err(s):
+ sys.stderr.write(s+'\n')
+
+ try:
+ main(args.FILE, verbose=args.verbose)
+ except KeyboardInterrupt:
+ if args.verbose:
+ traceback.print_exc()
+ err(str(e))
+ except zlib.error:
+ err('Failed to decompress decrypted data. Wrong password?')
+ sys.exit(os.EX_DATAERR)
+ except CalledProcessError as e:
+ if args.verbose:
+ traceback.print_exc()
+ print 'output:\n ' + e.output.replace('\n', '\n ').strip()
+ else:
+ err('CalledProcessError: ' + str(e))
+ sys.exit(os.EX_IOERR)
+ except IOError as e:
+ if args.verbose:
+ traceback.print_exc()
+ else:
+ err('IOError: ' + str(e))
+ sys.exit(os.EX_IOERR)