keepass2csv2pass.py (5225B)
1 #!/usr/bin/env python3 2 3 # Copyright 2015 David Francoeur <dfrancoeur04@gmail.com> 4 # Copyright 2017 Nathan Sommer <nsommer@wooster.edu> 5 # 6 # This file is licensed under the GPLv2+. Please see COPYING for more 7 # information. 8 # 9 # KeePassX 2+ on Mac allows export to CSV. The CSV contains the following 10 # headers: 11 # "Group","Title","Username","Password","URL","Notes" 12 # 13 # By default the pass entry will have the path Group/Title/Username and will 14 # have the following structure: 15 # 16 # <Password> 17 # user: <Username> 18 # url: <URL> 19 # notes: <Notes> 20 # 21 # Any missing fields will be omitted from the entry. If Username is not present 22 # the path will be Group/Title. 23 # 24 # The username can be left out of the path by using the --name_is_original 25 # switch. Group and Title can be converted to lowercase using the --to_lower 26 # switch. Groups can be excluded using the --exclude_groups option. 27 # 28 # Default usage: ./keepass2csv2pass.py input.csv 29 # 30 # To see the full usage: ./keepass2csv2pass.py -h 31 32 import sys 33 import csv 34 import argparse 35 from subprocess import Popen, PIPE 36 37 38 class KeepassCSVArgParser(argparse.ArgumentParser): 39 """ 40 Custom ArgumentParser class which prints the full usage message if the 41 input file is not provided. 42 """ 43 def error(self, message): 44 print(message, file=sys.stderr) 45 self.print_help() 46 sys.exit(2) 47 48 49 def pass_import_entry(path, data): 50 """Import new password entry to password-store using pass insert command""" 51 proc = Popen(['pass', 'insert', '--multiline', path], stdin=PIPE, 52 stdout=PIPE) 53 proc.communicate(data.encode('utf8')) 54 proc.wait() 55 56 57 def confirmation(prompt): 58 """ 59 Ask the user for 'y' or 'n' confirmation and return a boolean indicating 60 the user's choice. Returns True if the user simply presses enter. 61 """ 62 63 prompt = '{0} {1} '.format(prompt, '(Y/n)') 64 65 while True: 66 user_input = input(prompt) 67 68 if len(user_input) > 0: 69 first_char = user_input.lower()[0] 70 else: 71 first_char = 'y' 72 73 if first_char == 'y': 74 return True 75 elif first_char == 'n': 76 return False 77 78 print('Please enter y or n') 79 80 81 def insert_file_contents(filename, preparation_args): 82 """ Read the file and insert each entry """ 83 84 entries = [] 85 86 with open(filename, 'rU') as csv_in: 87 next(csv_in) 88 csv_out = (line for line in csv.reader(csv_in, dialect='excel')) 89 for row in csv_out: 90 path, data = prepare_for_insertion(row, **preparation_args) 91 if path and data: 92 entries.append((path, data)) 93 94 if len(entries) == 0: 95 return 96 97 print('Entries to import:') 98 99 for (path, data) in entries: 100 print(path) 101 102 if confirmation('Proceed?'): 103 for (path, data) in entries: 104 pass_import_entry(path, data) 105 print(path, 'imported!') 106 107 108 def prepare_for_insertion(row, name_is_username=True, convert_to_lower=False, 109 exclude_groups=None): 110 """Prepare a CSV row as an insertable string""" 111 112 group = escape(row[0]) 113 name = escape(row[1]) 114 115 # Bail if we are to exclude this group 116 if exclude_groups is not None: 117 for exclude_group in exclude_groups: 118 if exclude_group.lower() in group.lower(): 119 return None, None 120 121 # The first component of the group is 'Root', which we do not need 122 group_components = group.split('/')[1:] 123 124 path = '/'.join(group_components + [name]) 125 126 if convert_to_lower: 127 path = path.lower() 128 129 username = row[2] 130 password = row[3] 131 url = row[4] 132 notes = row[5] 133 134 if username and name_is_username: 135 path += '/' + username 136 137 data = '{}\n'.format(password) 138 139 if username: 140 data += 'user: {}\n'.format(username) 141 142 if url: 143 data += 'url: {}\n'.format(url) 144 145 if notes: 146 data += 'notes: {}\n'.format(notes) 147 148 return path, data 149 150 151 def escape(str_to_escape): 152 """ escape the list """ 153 return str_to_escape.replace(" ", "-")\ 154 .replace("&", "and")\ 155 .replace("[", "")\ 156 .replace("]", "") 157 158 159 def main(): 160 description = 'Import pass entries from an exported KeePassX CSV file.' 161 parser = KeepassCSVArgParser(description=description) 162 163 parser.add_argument('--exclude_groups', nargs='+', 164 help='Groups to exclude when importing') 165 parser.add_argument('--to_lower', action='store_true', 166 help='Convert group and name to lowercase') 167 parser.add_argument('--name_is_original', action='store_true', 168 help='Use the original entry name instead of the ' 169 'username for the pass entry') 170 parser.add_argument('input_file', help='The CSV file to read from') 171 172 args = parser.parse_args() 173 174 preparation_args = { 175 'convert_to_lower': args.to_lower, 176 'name_is_username': not args.name_is_original, 177 'exclude_groups': args.exclude_groups 178 } 179 180 input_file = args.input_file 181 print("File to read:", input_file) 182 insert_file_contents(input_file, preparation_args) 183 184 185 if __name__ == '__main__': 186 main()