aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorStefan Simroth <stefan.simroth@gmail.com>2013-05-18 21:23:16 +0200
committerJason A. Donenfeld <Jason@zx2c4.com>2013-05-19 02:00:26 +0200
commit7797980519ad8938e8290503ce721ef5f194c01f (patch)
treef680c8848922d40fb1a80c4741e79965b148cef6
parentSimplify sed to not use replacement. (diff)
downloadpassword-store-7797980519ad8938e8290503ce721ef5f194c01f.tar.xz
password-store-7797980519ad8938e8290503ce721ef5f194c01f.zip
Add Keepass2 import script.
-rwxr-xr-xcontrib/keepass2pass.py138
1 files changed, 138 insertions, 0 deletions
diff --git a/contrib/keepass2pass.py b/contrib/keepass2pass.py
new file mode 100755
index 0000000..16764bd
--- /dev/null
+++ b/contrib/keepass2pass.py
@@ -0,0 +1,138 @@
+#! /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')
+ 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:])