I have a confession. Sometimes I reuse passwords. Not for anything that “matters”, but I’ve ended up using a couple of passwords a lot of times. And inevitably some of those sites get hacked. But where did I use them?
Chrome remembers all my passwords but unfortunately doesn’t seem to offer a straightforward API to get at them. Conveniently they do sync my passwords to my computer’s password store and there’s an API for that.
I wrote a little script and I’ve been going through generating unique passwords for all the unimportant sites and turning two-factor authentication on where it’s offered. The Secret Service database (and Chrome) seem to sometimes end up with multiple entries for a single site, and Chrome doesn’t seem to sync my updates immediately, but I’ve found this a helpful start.
This file contains 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 | |
import argparse | |
import sys | |
import datetime | |
import secretstorage | |
import functools | |
def datetime_from_ss(ts): | |
datetime.datetime.fromtimestamp(int(ts) / 10000000) | |
@functools.total_ordering | |
class StoredPassword: | |
def __init__(self, item): | |
self.item = item | |
self.url = item.get_label() | |
self.password = item.get_secret().decode() | |
attributes = item.get_attributes() | |
self.realm = attributes['signon_realm'] | |
self.username = attributes['username_value'] | |
self.created = datetime_from_ss(attributes['date_created']) | |
self.synced = datetime_from_ss(attributes['date_synced']) | |
def __id__(self): | |
return id(self.item) | |
def __eq__(self, other): | |
return self.item == other.item | |
def __hash__(self): | |
return hash(self.item) | |
def __lt__(self, other): | |
return (self.created, self.synced) < (other.created, other.synced) | |
def __repr__(self): | |
date_format = '%Y-%m-%d' | |
return 'StoredPassword(%s %s %s %s %s)' % ( | |
self.realm, self.username, self.password, | |
self.created.strftime(date_format), | |
self.synced.strftime(date_format)) | |
def get_all_passwords(): | |
print('Loading passwords…') | |
bus = secretstorage.dbus_init() | |
collection = secretstorage.get_default_collection(bus) | |
collection.ensure_not_locked() | |
return [StoredPassword(item) | |
for item in collection.get_all_items() | |
if item.get_attributes()['xdg:schema'] == | |
'chrome_libsecret_password_schema'] | |
def password_for_site_matching(pattern, all_passwords): | |
matching = [item for item in all_passwords if pattern in item.realm] | |
pws = set(m.password for m in matching) | |
if len(pws) > 1: | |
print('Found %d passwords across %d password manager entries:') | |
for m in matching: | |
print(' %s %s' % (m.username, m.url)) | |
print('Use a more specific match.') | |
sys.exit(1) | |
return pws.pop() | |
def find_matching_passwords(password, site_pattern, all_passwords): | |
reuse = [item | |
for item in all_passwords | |
if item.password == password and (site_pattern is None or site_pattern not in item.realm)] | |
reuse.sort(key=lambda pw: pw.realm) | |
if len(reuse): | |
print('Found password. It is reused across %d sites:' % len(reuse)) | |
username_width = max(len(item.username) for item in reuse) | |
for item in reuse: | |
print('{:{width}} {}'.format(item.username, | |
item.realm, | |
width=username_width)) | |
if __name__ == '__main__': | |
parser = argparse.ArgumentParser(description='Find reused passwords') | |
parser.add_argument( | |
'–site', | |
type=str, | |
help='Find sites reusing a the password from this site') | |
parser.add_argument('–password', | |
type=str, | |
help='Find sites using this password') | |
args = parser.parse_args() | |
if not args.site and not args.password: | |
parser.print_help() | |
sys.exit(1) | |
if args.site and args.password: | |
parser.print_help() | |
print() | |
print('You can only pass one of –site and –password') | |
sys.exit(1) | |
all_passwords = get_all_passwords() | |
if args.site: | |
password = password_for_site_matching(args.site, all_passwords) | |
else: | |
password = args.password | |
find_matching_passwords(password, args.site, all_passwords) |