path: root/contrib/importers/password-exporter2pass.py
blob: 135feda1958fa4b2573619d6c43db3169ce4162f (plain) (tree)

#!/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'

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:
    parser = argparse.ArgumentParser(description=description)
        "filepath", type=str,
        help="The password Exporter generated file")
        "-p", "--prefix", type=str,
        help="Prefix for pass store path, you may want to use: sites")
        "-d", "--force", action="store_true",
        help="Call pass with --force option")
        "-v", "--verbose", action="store_true",
        help="Show pass output")
        "-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('#'):

        reader = csv.DictReader(csvfile, delimiter=',', quotechar='"')
        for row in reader:
                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:


                proc = subprocess.Popen(
                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
                print 'Error: corrupted line: {}'.format(row)

if __name__ == '__main__':