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()