#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2016 Daniele Pizzolli # # 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: """ 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 ": # 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()