Skip to content

Instantly share code, notes, and snippets.

@rounakdatta
Created August 29, 2020 21:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rounakdatta/eb6d0f13817eed56ac98b4f776f49428 to your computer and use it in GitHub Desktop.
Save rounakdatta/eb6d0f13817eed56ac98b4f776f49428 to your computer and use it in GitHub Desktop.
export from Chrome Passwords to standard pass
#!/usr/bin/env python3
# Copyright 2015 David Francoeur <dfrancoeur04@gmail.com>
# Copyright 2017 Nathan Sommer <nsommer@wooster.edu>
# Modified 2020 Rounak Datta <rounakdatta12@gmail.com>
#
# This file is licensed under the GPLv2+. Please see COPYING for more
# information.
#
# Chrome allows export passwords to CSV. The CSV contains the following
# headers:
# "Notes","URL","Username","Password"
#
# By default the pass entry will have the path Group/Title/Username and will
# have the following structure:
#
# <Password>
# user: <Username>
# url: <URL>
# notes: <Notes>
#
# Any missing fields will be omitted from the entry. If Username is not present
# the path will be Group/Title.
#
# The username can be left out of the path by using the --name_is_original
# switch. Group and Title can be converted to lowercase using the --to_lower
# switch. Groups can be excluded using the --exclude_groups option.
#
# Default usage: ./chrome2pass.py input.csv
#
# To see the full usage: ./chrome2pass.py -h
import sys
import csv
import argparse
from subprocess import Popen, PIPE
class ChromeCSVArgParser(argparse.ArgumentParser):
"""
Custom ArgumentParser class which prints the full usage message if the
input file is not provided.
"""
def error(self, message):
print(message, file=sys.stderr)
self.print_help()
sys.exit(2)
def pass_import_entry(path, data):
"""Import new password entry to password-store using pass insert command"""
proc = Popen(['pass', 'insert', '--multiline', path], stdin=PIPE,
stdout=PIPE)
proc.communicate(data.encode('utf8'))
proc.wait()
def confirmation(prompt):
"""
Ask the user for 'y' or 'n' confirmation and return a boolean indicating
the user's choice. Returns True if the user simply presses enter.
"""
prompt = '{0} {1} '.format(prompt, '(Y/n)')
while True:
user_input = input(prompt)
if len(user_input) > 0:
first_char = user_input.lower()[0]
else:
first_char = 'y'
if first_char == 'y':
return True
elif first_char == 'n':
return False
print('Please enter y or n')
def insert_file_contents(filename, preparation_args):
""" Read the file and insert each entry """
entries = []
with open(filename, 'rU') as csv_in:
next(csv_in)
csv_out = (line for line in csv.reader(csv_in, dialect='excel'))
for row in csv_out:
path, data = prepare_for_insertion(row, **preparation_args)
if path and data:
entries.append((path, data))
if len(entries) == 0:
return
print('Entries to import:')
for (path, data) in entries:
print(path)
if confirmation('Proceed?'):
for (path, data) in entries:
pass_import_entry(path, data)
print(path, 'imported!')
def prepare_for_insertion(row, name_is_username=True, convert_to_lower=False,
exclude_groups=None):
"""Prepare a CSV row as an insertable string"""
group = escape(row[0])
name = escape(row[1])
# Bail if we are to exclude this group
if exclude_groups is not None:
for exclude_group in exclude_groups:
if exclude_group.lower() in group.lower():
return None, None
# The first component of the group is 'Root', which we do not need
group_components = group.split('/')[1:]
path = '/'.join(group_components + [name])
if convert_to_lower:
path = path.lower()
print(row)
username = row[2]
password = row[3]
url = row[1]
notes = row[0]
if username and name_is_username:
path += '/' + username
data = '{}\n'.format(password)
if username:
data += 'user: {}\n'.format(username)
if url:
data += 'url: {}\n'.format(url)
if notes:
data += 'notes: {}\n'.format(notes)
return path, data
def escape(str_to_escape):
""" escape the list """
return str_to_escape.replace(" ", "-")\
.replace("&", "and")\
.replace("[", "")\
.replace("]", "")
def main():
description = 'Import pass entries from an exported Chrome Passwords CSV file.'
parser = ChromeCSVArgParser(description=description)
parser.add_argument('--exclude_groups', nargs='+',
help='Groups to exclude when importing')
parser.add_argument('--to_lower', action='store_true',
help='Convert group and name to lowercase')
parser.add_argument('--name_is_original', action='store_true',
help='Use the original entry name instead of the '
'username for the pass entry')
parser.add_argument('input_file', help='The CSV file to read from')
args = parser.parse_args()
preparation_args = {
'convert_to_lower': args.to_lower,
'name_is_username': not args.name_is_original,
'exclude_groups': args.exclude_groups
}
input_file = args.input_file
print("File to read:", input_file)
insert_file_contents(input_file, preparation_args)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment