From 5db9993af05ea358c0d5b3b270a40c37beb162f5 Mon Sep 17 00:00:00 2001
From: Nathan Sommer <nsommer@wooster.edu>
Date: Wed, 22 Feb 2017 18:10:07 -0500
Subject: keepasss2csv2pass: improve and make more flexible

- Code is now PEP 8 compliant
- Uses argparse module for command line arguments
- Prints what it will do and prompts for confirmation before
  proceeding
- Does not put URL and notes fields in the entry unless they
  are present in the CSV file
- Adds a "user" field in the entry
- There are now command line arguments for the following:
    - Exclude specific groups from being imported
    - Convert groups and names to lowercase
    - Use the name of the KeePass entry rather than the
      username as the pass entry name
---
 contrib/importers/keepass2csv2pass.py | 228 ++++++++++++++++++++++++++--------
 1 file changed, 176 insertions(+), 52 deletions(-)

(limited to 'contrib')

diff --git a/contrib/importers/keepass2csv2pass.py b/contrib/importers/keepass2csv2pass.py
index 47e787f..c3bd288 100755
--- a/contrib/importers/keepass2csv2pass.py
+++ b/contrib/importers/keepass2csv2pass.py
@@ -1,62 +1,186 @@
 #!/usr/bin/env python3
 
 # Copyright 2015 David Francoeur <dfrancoeur04@gmail.com>
-# This file is licensed under the GPLv2+. Please see COPYING for more information.
-
-# KeePassX 2+ on Mac allows export to CSV. The CSV contains the following headers :
+# Copyright 2017 Nathan Sommer <nsommer@wooster.edu>
+#
+# This file is licensed under the GPLv2+. Please see COPYING for more
+# information.
+#
+# KeePassX 2+ on Mac allows export to CSV. The CSV contains the following
+# headers:
 # "Group","Title","Username","Password","URL","Notes"
-# Group and Title are used to build the path, @see prepareForInsertion
-# Password is the first line and the url and the notes are appended after
 #
-# Usage: ./csv_to_pass.py test.csv
+# By default the pass entry will have the path Group/Title/Username and will
+# have the following structure:
+#
+# <Password>
+# user: <Username>
+# url: <URL>
+# notes: <Notes>
+#
+# Any missing fields will be omitted from the entry. If Username is not present
+# the path will be Group/Title.
+#
+# The username can be left out of the path by using the --name_is_original
+# switch. Group and Title can be converted to lowercase using the --to_lower
+# switch. Groups can be excluded using the --exclude_groups option.
+#
+# Default usage: ./keepass2csv2pass.py input.csv
+#
+# To see the full usage: ./keepass2csv2pass.py -h
 
-import csv
-import itertools
 import sys
+import csv
+import argparse
 from subprocess import Popen, PIPE
 
+
+class KeepassCSVArgParser(argparse.ArgumentParser):
+    """
+    Custom ArgumentParser class which prints the full usage message if the
+    input file is not provided.
+    """
+    def error(self, message):
+        print(message, file=sys.stderr)
+        self.print_help()
+        sys.exit(2)
+
+
 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 readFile(filename):
-	""" Read the file and proccess each entry """
-	with open(filename, 'rU') as csvIN:
-		next(csvIN)
-		outCSV=(line for line in csv.reader(csvIN, dialect='excel'))
-		#for row in itertools.islice(outCSV, 0, 1):
-		for row in outCSV:
-			#print("Length: ", len(row), row) 
-			prepareForInsertion(row)
-
-
-def prepareForInsertion(row):
-	""" prepare each CSV entry into an insertable string """
-	keyFolder = escape(row[0][4:])
-	keyName = escape(row[1])
-	username = row[2]
-	password = row[3]
-	url = row[4]
-	notes = row[5]
-	
-	path = keyFolder+"/"+keyName+"/"+username
-	data = password + "\n" if password else "\n"
-	data = "%s%s: %s\n" % (data, "url:", url+"\n")
-	data = "%s%s: %s\n" % (data, "notes:", notes+"\n")
-	pass_import_entry(path,data) 
-	print(path+" imported!")
-
-def escape(strToEscape):
-	""" escape the list """
-	return strToEscape.replace(" ", "-").replace("&","and").replace("[","").replace("]","")
-	
-
-def main(argv):
-	inputFile = sys.argv[1]
-	print("File to read: " + inputFile)
-	readFile(inputFile)
-
-
-main(sys.argv)
+    """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 confirmation(prompt):
+    """
+    Ask the user for 'y' or 'n' confirmation and return a boolean indicating
+    the user's choice. Returns True if the user simply presses enter.
+    """
+
+    prompt = '{0} {1} '.format(prompt, '(Y/n)')
+
+    while True:
+        user_input = input(prompt)
+
+        if len(user_input) > 0:
+            first_char = user_input.lower()[0]
+        else:
+            first_char = 'y'
+
+        if first_char == 'y':
+            return True
+        elif first_char == 'n':
+            return False
+
+        print('Please enter y or n')
+
+
+def insert_file_contents(filename, preparation_args):
+    """ Read the file and insert each entry """
+
+    entries = []
+
+    with open(filename, 'rU') as csv_in:
+        next(csv_in)
+        csv_out = (line for line in csv.reader(csv_in, dialect='excel'))
+        for row in csv_out:
+            path, data = prepare_for_insertion(row, **preparation_args)
+            if path and data:
+                entries.append((path, data))
+
+    if len(entries) == 0:
+        return
+
+    print('Entries to import:')
+
+    for (path, data) in entries:
+        print(path)
+
+    if confirmation('Proceed?'):
+        for (path, data) in entries:
+            pass_import_entry(path, data)
+            print(path, 'imported!')
+
+
+def prepare_for_insertion(row, name_is_username=True, convert_to_lower=False,
+                          exclude_groups=None):
+    """Prepare a CSV row as an insertable string"""
+
+    group = escape(row[0])
+    name = escape(row[1])
+
+    # Bail if we are to exclude this group
+    if exclude_groups is not None:
+        for exclude_group in exclude_groups:
+            if exclude_group.lower() in group.lower():
+                return None, None
+
+    # The first component of the group is 'Root', which we do not need
+    group_components = group.split('/')[1:]
+
+    path = '/'.join(group_components + [name])
+
+    if convert_to_lower:
+        path = path.lower()
+
+    username = row[2]
+    password = row[3]
+    url = row[4]
+    notes = row[5]
+
+    if username and name_is_username:
+        path += '/' + username
+
+    data = '{}\n'.format(password)
+
+    if username:
+        data += 'user: {}\n'.format(username)
+
+    if url:
+        data += 'url: {}\n'.format(url)
+
+    if notes:
+        data += 'notes: {}\n'.format(notes)
+
+    return path, data
+
+
+def escape(str_to_escape):
+    """ escape the list """
+    return str_to_escape.replace(" ", "-")\
+                        .replace("&", "and")\
+                        .replace("[", "")\
+                        .replace("]", "")
+
+
+def main():
+    description = 'Import pass entries from an exported KeePassX CSV file.'
+    parser = KeepassCSVArgParser(description=description)
+
+    parser.add_argument('--exclude_groups', nargs='+',
+                        help='Groups to exclude when importing')
+    parser.add_argument('--to_lower', action='store_true',
+                        help='Convert group and name to lowercase')
+    parser.add_argument('--name_is_original', action='store_true',
+                        help='Use the original entry name instead of the '
+                             'username for the pass entry')
+    parser.add_argument('input_file', help='The CSV file to read from')
+
+    args = parser.parse_args()
+
+    preparation_args = {
+        'convert_to_lower': args.to_lower,
+        'name_is_username': not args.name_is_original,
+        'exclude_groups': args.exclude_groups
+    }
+
+    input_file = args.input_file
+    print("File to read:", input_file)
+    insert_file_contents(input_file, preparation_args)
+
+
+if __name__ == '__main__':
+    main()
-- 
cgit v1.2.3