Skip to content

Commit

Permalink
Check for duplicate / re-used passwords.
Browse files Browse the repository at this point in the history
Fix #22.
  • Loading branch information
roddhjav committed Jan 30, 2022
1 parent a8f23a0 commit 119dfdd
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 6 deletions.
18 changes: 12 additions & 6 deletions pass_audit/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ class ArgParser(ArgumentParser):
def __init__(self):
description = """
A pass extension for auditing your password repository. It supports safe
breached password detection from haveibeenpwned.com using K-anonymity method
and password strength estimaton using zxcvbn."""
breached password detection from haveibeenpwned.com using K-anonymity method,
duplicated passwords, and password strength estimaton using zxcvbn."""
epilog = "More information may be found in the pass-audit(1) man page."

super().__init__(prog='pass audit',
Expand Down Expand Up @@ -135,12 +135,18 @@ def main():
msg.warning(f"Weak password detected: {payload} from {path}"
f" might be weak. {zxcvbn_parse(details)}")

if not breached and not weak:
msg.success(
f"None of the {len(data)} passwords tested are breached or weak.")
msg.verbose("Checking for duplicated passwords")
duplicated = audit.duplicates()
for paths in duplicated:
msg.warning(f"Duplicated passwords detected in {', '.join(paths)}")

if not breached and not weak and not duplicated:
msg.success(f"None of the {len(data)} passwords tested are "
"breached, duplicated or weak.")
else:
msg.error(f"{len(data)} passwords tested and {len(breached)} breached,"
f" {len(weak)} weak passwords found.")
f" {len(weak)} weak passwords found,"
f" {len(duplicated)} duplicated passwords found.")
msg.message("You should update them with 'pass update'.")


Expand Down
18 changes: 18 additions & 0 deletions pass_audit/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,21 @@ def zxcvbn(self):
if results['score'] <= 2:
weak.append((path, password, results))
return weak

def duplicates(self):
"""Check for duplicated passwords."""
seen = {}
for path, entry in self.data.items():
if entry.get('password', '') == '':
continue
password = entry['password']
if password in seen:
seen[password].append(path)
else:
seen[password] = [path]

duplicated = []
for paths in seen.values():
if len(paths) > 1:
duplicated.append(paths)
return duplicated
17 changes: 17 additions & 0 deletions tests/test_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,28 @@ def test_zxcvbn_strong(self):
weak = audit.zxcvbn()
self.assertTrue(len(weak) == 0)

def test_duplicates_yes(self):
"""Testing: pass audit for duplicates password."""
data = tests.getdata('Password/notpwned/1')
data['Password/notpwned/copy'] = data['Password/notpwned/1']
audit = pass_audit.audit.PassAudit(data, True)
duplicated = audit.duplicates()
self.assertTrue(len(duplicated) == 1)

def test_duplicates_no(self):
"""Testing: pass audit for not duplicated password."""
data = tests.getdata('Password/notpwned/')
audit = pass_audit.audit.PassAudit(data, True)
duplicated = audit.duplicates()
self.assertTrue(len(duplicated) == 0)

def test_empty(self):
"""Testing: pass audit for empty password."""
data = {'empty': {'password': ''}}
audit = pass_audit.audit.PassAudit(data, self.msg)
weak = audit.zxcvbn()
breached = audit.password()
duplicated = audit.duplicates()
self.assertTrue(len(weak) == 0)
self.assertTrue(len(breached) == 0)
self.assertTrue(len(duplicated) == 0)
10 changes: 10 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#

import os
import shutil
from unittest import mock

from pass_audit.passwordstore import PasswordStore
Expand Down Expand Up @@ -81,6 +82,15 @@ def test_main_passwords_pwned(self):
cmd = ['Password/pwned']
self.main(cmd)

@mock.patch('requests.get', tests.mock_request)
def test_main_passwords_duplicate(self):
"""Testing: pass audit for duplicates."""
shutil.copy(os.path.join(self.store.prefix, 'Password/good/1.gpg'),
os.path.join(self.store.prefix, 'Password/good/10.gpg'))
cmd = ['Password/good']
self.main(cmd)
os.remove(os.path.join(self.store.prefix, 'Password/good/10.gpg'))

@mock.patch('requests.get', tests.mock_request)
def test_main_passwords_good(self):
"""Testing: pass audit Password/good."""
Expand Down

0 comments on commit 119dfdd

Please sign in to comment.