password-store

Simple password manager using gpg and ordinary unix directories
git clone https://git.zx2c4.com/password-store
Log | Files | Refs | README | LICENSE

password-exporter2pass.py (6510B)


      1 #!/usr/bin/env python
      2 # -*- coding: utf-8 -*-
      3 
      4 # Copyright (C) 2016 Daniele Pizzolli <daniele.pizzolli@create-net.org>
      5 #
      6 # This file is licensed under GPLv2+. Please see COPYING for more
      7 # information.
      8 
      9 """Import password(s) exported by Password Exporter for Firefox in
     10 csv format to pass format.  Supports Password Exporter format 1.1.
     11 """
     12 
     13 import argparse
     14 import base64
     15 import csv
     16 import sys
     17 import subprocess
     18 
     19 
     20 PASS_PROG = 'pass'
     21 DEFAULT_USERNAME = 'login'
     22 
     23 
     24 def main():
     25     "Parse the arguments and run the passimport with appropriate arguments."
     26     description = """\
     27     Import password(s) exported by Password Exporter for Firefox in csv
     28     format to pass format.  Supports Password Exporter format 1.1.
     29 
     30     Check the first line of your exported file.
     31 
     32     Must start with:
     33 
     34     # Generated by Password Exporter; Export format 1.1;
     35 
     36     Support obfuscated export (wrongly called encrypted by Password Exporter).
     37 
     38     It should help you to migrate from the default Firefox password
     39     store to passff.
     40 
     41     Please note that Password Exporter or passff may have problem with
     42     fields containing characters like " or :.
     43 
     44     More info at:
     45     <https://addons.mozilla.org/en-US/firefox/addon/password-exporter>
     46     <https://addons.mozilla.org/en-US/firefox/addon/passff>
     47     """
     48     parser = argparse.ArgumentParser(description=description)
     49     parser.add_argument(
     50         "filepath", type=str,
     51         help="The password Exporter generated file")
     52     parser.add_argument(
     53         "-p", "--prefix", type=str,
     54         help="Prefix for pass store path, you may want to use: sites")
     55     parser.add_argument(
     56         "-d", "--force", action="store_true",
     57         help="Call pass with --force option")
     58     parser.add_argument(
     59         "-v", "--verbose", action="store_true",
     60         help="Show pass output")
     61     parser.add_argument(
     62         "-q", "--quiet", action="store_true",
     63         help="No output")
     64 
     65     args = parser.parse_args()
     66 
     67     passimport(args.filepath, prefix=args.prefix, force=args.force,
     68                verbose=args.verbose, quiet=args.quiet)
     69 
     70 
     71 def passimport(filepath, prefix=None, force=False, verbose=False, quiet=False):
     72     "Import the password from filepath to pass"
     73     with open(filepath, 'rb') as csvfile:
     74         # Skip the first line if starts with a comment, as usually are
     75         # file exported with Password Exporter
     76         first_line = csvfile.readline()
     77 
     78         if not first_line.startswith(
     79                 '# Generated by Password Exporter; Export format 1.1;'):
     80             sys.exit('Input format not supported')
     81 
     82         # Auto detect if the file is obfuscated
     83         obfuscation = False
     84         if first_line.startswith(
     85                 ('# Generated by Password Exporter; '
     86                  'Export format 1.1; Encrypted: true')):
     87             obfuscation = True
     88 
     89         if not first_line.startswith('#'):
     90             csvfile.seek(0)
     91 
     92         reader = csv.DictReader(csvfile, delimiter=',', quotechar='"')
     93         for row in reader:
     94             try:
     95                 username = row['username']
     96                 password = row['password']
     97 
     98                 if obfuscation:
     99                     username = base64.b64decode(row['username'])
    100                     password = base64.b64decode(row['password'])
    101 
    102                 # Not sure if some fiel can be empty, anyway tries to be
    103                 # reasonably safe
    104                 text = '{}\n'.format(password)
    105                 if row['passwordField']:
    106                     text += '{}: {}\n'.format(row['passwordField'], password)
    107                 if username:
    108                     text += '{}: {}\n'.format(
    109                         row.get('usernameField', DEFAULT_USERNAME), username)
    110                 if row['hostname']:
    111                     text += 'Hostname: {}\n'.format(row['hostname'])
    112                 if row['httpRealm']:
    113                     text += 'httpRealm: {}\n'.format(row['httpRealm'])
    114                 if row['formSubmitURL']:
    115                     text += 'formSubmitURL: {}\n'.format(row['formSubmitURL'])
    116 
    117                 # Remove the protocol prefix for http(s)
    118                 simplename = row['hostname'].replace(
    119                     'https://', '').replace('http://', '')
    120 
    121                 # Rough protection for fancy username like ā€œ; rm -Rf /\nā€
    122                 userpath = "".join(x for x in username if x.isalnum())
    123                 # TODO add some escape/protection also to the hostname
    124                 storename = '{}@{}'.format(userpath, simplename)
    125                 storepath = storename
    126 
    127                 if prefix:
    128                     storepath = '{}/{}'.format(prefix, storename)
    129 
    130                 cmd = [PASS_PROG, 'insert', '--multiline']
    131 
    132                 if force:
    133                     cmd.append('--force')
    134 
    135                 cmd.append(storepath)
    136 
    137                 proc = subprocess.Popen(
    138                     cmd,
    139                     stdin=subprocess.PIPE,
    140                     stdout=subprocess.PIPE,
    141                     stderr=subprocess.PIPE)
    142                 stdout, stderr = proc.communicate(text)
    143                 retcode = proc.wait()
    144 
    145                 # TODO: please note that sometimes pass does not return an
    146                 # error
    147                 #
    148                 # After this command:
    149                 #
    150                 # pass git config --bool --add pass.signcommits true
    151                 #
    152                 # pass import will fail with:
    153                 #
    154                 # gpg: skipped "First Last <user@example.com>":
    155                 #    secret key not available
    156                 # gpg: signing failed: secret key not available
    157                 # error: gpg failed to sign the data
    158                 # fatal: failed to write commit object
    159                 #
    160                 # But the retcode is still 0.
    161                 #
    162                 # Workaround: add the first signing key id explicitly with:
    163                 #
    164                 # SIGKEY=$(gpg2 --list-keys --with-colons user@example.com | \
    165                 #     awk -F : '/:s:$/ {printf "0x%s\n", $5; exit}')
    166                 # pass git config --add user.signingkey "${SIGKEY}"
    167 
    168                 if retcode:
    169                     print 'command {}" failed with exit code {}: {}'.format(
    170                         " ".join(cmd), retcode, stdout + stderr)
    171 
    172                 if not quiet:
    173                     print 'Imported {}'.format(storepath)
    174 
    175                 if verbose:
    176                     print stdout + stderr
    177             except:
    178                 print 'Error: corrupted line: {}'.format(row)
    179 
    180 if __name__ == '__main__':
    181     main()