password-store

Simple password manager using gpg and ordinary unix directories
git clone https://git.zx2c4.com/password-store
Log | Files | Refs | README | LICENSE

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()