summaryrefslogtreecommitdiff
path: root/contrib/importers
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xcontrib/importers/password-exporter2pass.py181
1 files changed, 181 insertions, 0 deletions
diff --git a/contrib/importers/password-exporter2pass.py b/contrib/importers/password-exporter2pass.py
new file mode 100755
index 0000000..135feda
--- /dev/null
+++ b/contrib/importers/password-exporter2pass.py
@@ -0,0 +1,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()