aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/contrib/importers/password-exporter2pass.py
blob: 135feda1958fa4b2573619d6c43db3169ce4162f (plain)
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
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright (C) 2016 Daniele Pizzolli <daniele.pizzolli@create-net.org>
#
# This file is licensed under GPLv2+. Please see COPYING for more
# information.

"""Import password(s) exported by Password Exporter for Firefox in
csv format to pass format.  Supports Password Exporter format 1.1.
"""

import argparse
import base64
import csv
import sys
import subprocess


PASS_PROG = 'pass'
DEFAULT_USERNAME = 'login'


def main():
    "Parse the arguments and run the passimport with appropriate arguments."
    description = """\
    Import password(s) exported by Password Exporter for Firefox in csv
    format to pass format.  Supports Password Exporter format 1.1.

    Check the first line of your exported file.

    Must start with:

    # Generated by Password Exporter; Export format 1.1;

    Support obfuscated export (wrongly called encrypted by Password Exporter).

    It should help you to migrate from the default Firefox password
    store to passff.

    Please note that Password Exporter or passff may have problem with
    fields containing characters like " or :.

    More info at:
    <https://addons.mozilla.org/en-US/firefox/addon/password-exporter>
    <https://addons.mozilla.org/en-US/firefox/addon/passff>
    """
    parser = argparse.ArgumentParser(description=description)
    parser.add_argument(
        "filepath", type=str,
        help="The password Exporter generated file")
    parser.add_argument(
        "-p", "--prefix", type=str,
        help="Prefix for pass store path, you may want to use: sites")
    parser.add_argument(
        "-d", "--force", action="store_true",
        help="Call pass with --force option")
    parser.add_argument(
        "-v", "--verbose", action="store_true",
        help="Show pass output")
    parser.add_argument(
        "-q", "--quiet", action="store_true",
        help="No output")

    args = parser.parse_args()

    passimport(args.filepath, prefix=args.prefix, force=args.force,
               verbose=args.verbose, quiet=args.quiet)


def passimport(filepath, prefix=None, force=False, verbose=False, quiet=False):
    "Import the password from filepath to pass"
    with open(filepath, 'rb') as csvfile:
        # Skip the first line if starts with a comment, as usually are
        # file exported with Password Exporter
        first_line = csvfile.readline()

        if not first_line.startswith(
                '# Generated by Password Exporter; Export format 1.1;'):
            sys.exit('Input format not supported')

        # Auto detect if the file is obfuscated
        obfuscation = False
        if first_line.startswith(
                ('# Generated by Password Exporter; '
                 'Export format 1.1; Encrypted: true')):
            obfuscation = True

        if not first_line.startswith('#'):
            csvfile.seek(0)

        reader = csv.DictReader(csvfile, delimiter=',', quotechar='"')
        for row in reader:
            try:
                username = row['username']
                password = row['password']

                if obfuscation:
                    username = base64.b64decode(row['username'])
                    password = base64.b64decode(row['password'])

                # Not sure if some fiel can be empty, anyway tries to be
                # reasonably safe
                text = '{}\n'.format(password)
                if row['passwordField']:
                    text += '{}: {}\n'.format(row['passwordField'], password)
                if username:
                    text += '{}: {}\n'.format(
                        row.get('usernameField', DEFAULT_USERNAME), username)
                if row['hostname']:
                    text += 'Hostname: {}\n'.format(row['hostname'])
                if row['httpRealm']:
                    text += 'httpRealm: {}\n'.format(row['httpRealm'])
                if row['formSubmitURL']:
                    text += 'formSubmitURL: {}\n'.format(row['formSubmitURL'])

                # Remove the protocol prefix for http(s)
                simplename = row['hostname'].replace(
                    'https://', '').replace('http://', '')

                # Rough protection for fancy username like ā€œ; rm -Rf /\nā€
                userpath = "".join(x for x in username if x.isalnum())
                # TODO add some escape/protection also to the hostname
                storename = '{}@{}'.format(userpath, simplename)
                storepath = storename

                if prefix:
                    storepath = '{}/{}'.format(prefix, storename)

                cmd = [PASS_PROG, 'insert', '--multiline']

                if force:
                    cmd.append('--force')

                cmd.append(storepath)

                proc = subprocess.Popen(
                    cmd,
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE)
                stdout, stderr = proc.communicate(text)
                retcode = proc.wait()

                # TODO: please note that sometimes pass does not return an
                # error
                #
                # After this command:
                #
                # pass git config --bool --add pass.signcommits true
                #
                # pass import will fail with:
                #
                # gpg: skipped "First Last <user@example.com>":
                #    secret key not available
                # gpg: signing failed: secret key not available
                # error: gpg failed to sign the data
                # fatal: failed to write commit object
                #
                # But the retcode is still 0.
                #
                # Workaround: add the first signing key id explicitly with:
                #
                # SIGKEY=$(gpg2 --list-keys --with-colons user@example.com | \
                #     awk -F : '/:s:$/ {printf "0x%s\n", $5; exit}')
                # pass git config --add user.signingkey "${SIGKEY}"

                if retcode:
                    print 'command {}" failed with exit code {}: {}'.format(
                        " ".join(cmd), retcode, stdout + stderr)

                if not quiet:
                    print 'Imported {}'.format(storepath)

                if verbose:
                    print stdout + stderr
            except:
                print 'Error: corrupted line: {}'.format(row)

if __name__ == '__main__':
    main()