aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorNathan Sommer <nsommer@wooster.edu>2017-02-22 18:10:07 -0500
committerJason A. Donenfeld <Jason@zx2c4.com>2017-02-25 11:18:09 +0100
commit5db9993af05ea358c0d5b3b270a40c37beb162f5 (patch)
tree5fa0826fa9694969b25d8d06bee156352abeeae9
parentshow,generate: support qrcodes (diff)
downloadpassword-store-5db9993af05ea358c0d5b3b270a40c37beb162f5.tar.xz
password-store-5db9993af05ea358c0d5b3b270a40c37beb162f5.zip
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
-rwxr-xr-xcontrib/importers/keepass2csv2pass.py228
1 files changed, 176 insertions, 52 deletions
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()