Created
August 29, 2020 21:41
-
-
Save rounakdatta/eb6d0f13817eed56ac98b4f776f49428 to your computer and use it in GitHub Desktop.
export from Chrome Passwords to standard pass
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Copyright 2015 David Francoeur <[email protected]> | |
# Copyright 2017 Nathan Sommer <[email protected]> | |
# Modified 2020 Rounak Datta <[email protected]> | |
# | |
# 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