Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions keepercommander/commands/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
from ..params import KeeperParams, LAST_RECORD_UID, LAST_FOLDER_UID, LAST_SHARED_FOLDER_UID
from ..proto import ssocloud_pb2, enterprise_pb2, APIRequest_pb2
from ..security_audit import needs_security_audit, update_security_audit_data
from ..utils import password_score
from ..utils import password_score, master_password_score
from ..vault import KeeperRecord
from ..versioning import is_binary_app, is_up_to_date_version

Expand Down Expand Up @@ -2319,6 +2319,9 @@ def execute(self, params, **kwargs):
logging.warning('Password rules:\n%s', '\n'.join((f' {x}' for x in failed_rules)))
return

score = utils.master_password_score(new_password)
logging.info('Password strength: %s', 'WEAK' if score <= 25 else 'FAIR' if score == 50 else 'MEDIUM' if score == 75 else 'STRONG')

if params.breach_watch:
euids = []
for result in params.breach_watch.scan_passwords(params, [new_password]):
Expand All @@ -2327,9 +2330,6 @@ def execute(self, params, **kwargs):
logging.info('Breachwatch password scan result: %s', 'WEAK' if result[1].breachDetected else 'GOOD')
if euids:
params.breach_watch.delete_euids(params, euids)
else:
score = utils.password_score(new_password)
logging.info('Password strength: %s', 'WEAK' if score < 40 else 'FAIR' if score < 60 else 'MEDIUM' if score < 80 else 'STRONG')

iterations = current_salt.iterations if current_salt else constants.PBKDF2_ITERATIONS
iterations = max(iterations, constants.PBKDF2_ITERATIONS)
Expand Down
15 changes: 15 additions & 0 deletions keepercommander/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

from . import crypto
from .constants import EMAIL_PATTERN
import zxcvbn as _zxcvbn


VALID_URL_SCHEME_CHARS = '+-.:'
Expand Down Expand Up @@ -429,6 +430,20 @@ def is_pw_strong(pw_score): # type: (int) -> bool
return pw_score >= 80


_MASTER_PASSWORD_SCORE_MAP = {0: 25, 1: 25, 2: 50, 3: 75, 4: 100}


def master_password_score(password): # type: (str) -> int
if not password or not isinstance(password, str):
return 0
try:
result = _zxcvbn.zxcvbn(password)
return _MASTER_PASSWORD_SCORE_MAP.get(result.get('score'), 25)
except Exception as e:
logging.debug('zxcvbn scoring failed: %s', e)
return 25


def is_rec_at_risk(bw_result): # type (int) -> bool
return bw_result in (2, 3)

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
asciitree
zxcvbn
bcrypt
colorama
prompt_toolkit
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ include_package_data = True
install_requires =
asciitree
bcrypt
zxcvbn
colorama
cryptography>=41.0.0
fido2>=2.0.0; python_version>='3.10'
Expand Down
8 changes: 8 additions & 0 deletions unit-tests/test_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ def test_password_score(self):
self.assertEqual(utils.password_score('AAAbbbCCC11'), 38)
self.assertEqual(utils.password_score('password'), 8)

def test_master_password_score(self):
# zxcvbn-based scoring: returns 25 (Weak), 50 (Fair), 75 (Medium), or 100 (Strong)
self.assertEqual(utils.master_password_score('!@#$%^&*()'), 25) # zxcvbn score 1 -> Weak
self.assertEqual(utils.master_password_score('aZkljfzsnmp4w9058dsqln5yf(&*))(*)(345'), 100) # zxcvbn score 4 -> Strong
self.assertEqual(utils.master_password_score('c3>^sxuKZ[Ndyo(OBE14'), 100) # zxcvbn score 4 -> Strong
self.assertEqual(utils.master_password_score('AAAbbbCCC11'), 75) # zxcvbn score 3 -> Medium
self.assertEqual(utils.master_password_score('password'), 25) # zxcvbn score 0 -> Weak


_test_random_data = \
'cKGoVph_X0NKjk8jQgxyQWRElUY7IsbbIJaRcJVlnOb7AchFiY-izmTTOlgArwIqAxKDKSRAWx2Q1pX' \
Expand Down
Loading