aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/contrib/importers/keepass2csv2pass.py
blob: c3bd2884ec1d6470cbc7750c07ec2d378a174f7d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#!/usr/bin/env python3

# Copyright 2015 David Francoeur <dfrancoeur04@gmail.com>
# 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"
#
# 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 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 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()