From 26c6b8ea5d3626f6636206f6bdc68e22c4c4a034 Mon Sep 17 00:00:00 2001 From: Gabe Wachob Date: Fri, 5 Dec 2014 17:16:55 -0800 Subject: [PATCH 1/4] Make zxcvbn py2 and py3 compatible in one codebase using "future" package --- setup.py | 3 ++ zxcvbn/__init__.py | 7 ++-- zxcvbn/matching.py | 35 +++++++++++++------ zxcvbn/scoring.py | 22 +++++++----- zxcvbn/scripts/build_frequency_lists.py | 34 ++++++++++-------- .../scripts/build_keyboard_adjacency_graph.py | 2 +- 6 files changed, 64 insertions(+), 39 deletions(-) diff --git a/setup.py b/setup.py index 24af8dc..32b781c 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,8 @@ from distutils.core import setup +requires = [ + 'future' +] setup(name='zxcvbn', version='1.0', description='Password strength estimator', diff --git a/zxcvbn/__init__.py b/zxcvbn/__init__.py index 7b444a4..ad99e97 100644 --- a/zxcvbn/__init__.py +++ b/zxcvbn/__init__.py @@ -1,3 +1,4 @@ +from __future__ import print_function from zxcvbn import main __all__ = ['password_strength'] @@ -11,8 +12,8 @@ for line in fileinput.input(): pw = line.strip() - print "Password: " + pw + print("Password: " + pw) out = password_strength(pw) - for key, value in out.iteritems(): + for key, value in out.items(): if key not in ignored: - print "\t%s: %s" % (key, value) + print("\t%s: %s" % (key, value)) diff --git a/zxcvbn/matching.py b/zxcvbn/matching.py index a5b31a8..f20de1b 100644 --- a/zxcvbn/matching.py +++ b/zxcvbn/matching.py @@ -1,6 +1,10 @@ +from builtins import map +from builtins import str +from builtins import range from itertools import groupby import pkg_resources import re +import sys try: import simplejson as json @@ -8,6 +12,11 @@ except ImportError: import json +if (sys.version_info[0] > 2): + PY3=True +else: + PY3=False + GRAPHS = {} DICTIONARY_MATCHERS = [] @@ -29,8 +38,8 @@ def dictionary_match(password, ranked_dict): pw_lower = password.lower() - for i in xrange(0, length): - for j in xrange(i, length): + for i in range(0, length): + for j in range(i, length): word = pw_lower[i:j+1] if word in ranked_dict: rank = ranked_dict[word] @@ -64,14 +73,18 @@ def _build_ranked_dict(unranked_list): def _load_frequency_lists(): data = pkg_resources.resource_string(__name__, 'generated/frequency_lists.json') + if PY3: + data = data.decode('utf-8') dicts = json.loads(data) - for name, wordlist in dicts.items(): + for name, wordlist in list(dicts.items()): DICTIONARY_MATCHERS.append(_build_dict_matcher(name, _build_ranked_dict(wordlist))) def _load_adjacency_graphs(): global GRAPHS data = pkg_resources.resource_string(__name__, 'generated/adjacency_graphs.json') + if PY3: + data = data.decode('utf-8') GRAPHS = json.loads(data) @@ -79,7 +92,7 @@ def _load_adjacency_graphs(): # this calculates the average over all keys. def _calc_average_degree(graph): average = 0.0 - for neighbors in graph.values(): + for neighbors in list(graph.values()): average += len([n for n in neighbors if n is not None]) average /= len(graph) @@ -122,7 +135,7 @@ def relevant_l33t_subtable(password): password_chars = set(password) filtered = {} - for letter, subs in L33T_TABLE.items(): + for letter, subs in list(L33T_TABLE.items()): relevent_subs = [sub for sub in subs if sub in password_chars] if len(relevent_subs) > 0: filtered[letter] = relevent_subs @@ -142,7 +155,7 @@ def dedup(subs): deduped.append(sub) return deduped - keys = table.keys() + keys = list(table.keys()) while len(keys) > 0: first_key = keys[0] rest_keys = keys[1:] @@ -166,7 +179,7 @@ def dedup(subs): next_subs.append(sub_alternative) subs = dedup(next_subs) keys = rest_keys - return map(dict, subs) + return list(map(dict, subs)) def l33t_match(password): @@ -182,13 +195,13 @@ def l33t_match(password): if token.lower() == match['matched_word']: continue match_sub = {} - for subbed_chr, char in sub.items(): + for subbed_chr, char in list(sub.items()): if token.find(subbed_chr) != -1: match_sub[subbed_chr] = char match['l33t'] = True match['token'] = token match['sub'] = match_sub - match['sub_display'] = ', '.join([("%s -> %s" % (k, v)) for k, v in match_sub.items()]) + match['sub_display'] = ', '.join([("%s -> %s" % (k, v)) for k, v in list(match_sub.items())]) matches.append(match) return matches @@ -198,7 +211,7 @@ def l33t_match(password): def spatial_match(password): matches = [] - for graph_name, graph in GRAPHS.items(): + for graph_name, graph in list(GRAPHS.items()): matches.extend(spatial_match_helper(password, graph, graph_name)) return matches @@ -293,7 +306,7 @@ def sequence_match(password): seq = None # either lower, upper, or digits seq_name = None seq_direction = None # 1 for ascending seq abcd, -1 for dcba - for seq_candidate_name, seq_candidate in SEQUENCES.items(): + for seq_candidate_name, seq_candidate in list(SEQUENCES.items()): i_n = seq_candidate.find(password[i]) j_n = seq_candidate.find(password[j]) if j < len(password) else -1 diff --git a/zxcvbn/scoring.py b/zxcvbn/scoring.py index 8903f53..1a0c8ba 100644 --- a/zxcvbn/scoring.py +++ b/zxcvbn/scoring.py @@ -1,3 +1,7 @@ +from __future__ import division +from builtins import str +from builtins import range +from past.utils import old_div import math import re @@ -123,7 +127,7 @@ def round_to_x_digits(number, digits): """ Returns 'number' rounded to 'digits' digits. """ - return round(number * math.pow(10, digits)) / math.pow(10, digits) + return old_div(round(number * math.pow(10, digits)), math.pow(10, digits)) # ------------------------------------------------------------------------------ # threat model -- stolen hash catastrophe scenario ----------------------------- @@ -143,7 +147,7 @@ def round_to_x_digits(number, digits): SINGLE_GUESS = .010 NUM_ATTACKERS = 100 # number of cores guessing in parallel. -SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS +SECONDS_PER_GUESS = old_div(SINGLE_GUESS, NUM_ATTACKERS) def entropy_to_crack_time(entropy): @@ -253,7 +257,7 @@ def spatial_entropy(match): if 'shifted_count' in match: S = match['shifted_count'] U = L - S # unshifted count - possibilities = sum(binom(S + U, i) for i in xrange(0, min(S, U) + 1)) + possibilities = sum(binom(S + U, i) for i in range(0, min(S, U) + 1)) entropy += lg(possibilities) return entropy @@ -294,7 +298,7 @@ def extra_l33t_entropy(match): if 'l33t' not in match or not match['l33t']: return 0 possibilities = 0 - for subbed, unsubbed in match['sub'].items(): + for subbed, unsubbed in list(match['sub'].items()): sub_len = len([x for x in match['token'] if x == subbed]) unsub_len = len([x for x in match['token'] if x == unsubbed]) possibilities += sum(binom(unsub_len + sub_len, i) for i in range(0, min(unsub_len, sub_len) + 1)) @@ -330,14 +334,14 @@ def display_time(seconds): if seconds < minute: return 'instant' elif seconds < hour: - return str(1 + math.ceil(seconds / minute)) + " minutes" + return str(1 + math.ceil(old_div(seconds, minute))) + " minutes" elif seconds < day: - return str(1 + math.ceil(seconds / hour)) + " hours" + return str(1 + math.ceil(old_div(seconds, hour))) + " hours" elif seconds < month: - return str(1 + math.ceil(seconds / day)) + " days" + return str(1 + math.ceil(old_div(seconds, day))) + " days" elif seconds < year: - return str(1 + math.ceil(seconds / month)) + " months" + return str(1 + math.ceil(old_div(seconds, month))) + " months" elif seconds < century: - return str(1 + math.ceil(seconds / year)) + " years" + return str(1 + math.ceil(old_div(seconds, year))) + " years" else: return 'centuries' diff --git a/zxcvbn/scripts/build_frequency_lists.py b/zxcvbn/scripts/build_frequency_lists.py index 60c61ab..840c6cd 100644 --- a/zxcvbn/scripts/build_frequency_lists.py +++ b/zxcvbn/scripts/build_frequency_lists.py @@ -1,3 +1,7 @@ +from __future__ import print_function +from future import standard_library +standard_library.install_aliases() +from builtins import range from __future__ import with_statement import os import time @@ -8,7 +12,7 @@ except ImportError: import json -import urllib2 +import urllib.request, urllib.error, urllib.parse SLEEP_TIME = 20 # seconds @@ -24,11 +28,11 @@ def get_ranked_english(): ''' URL_TMPL = 'http://en.wiktionary.org/wiki/Wiktionary:Frequency_lists/TV/2006/%s' urls = [] - for i in xrange(10): + for i in range(10): freq_range = "%d-%d" % (i * 1000 + 1, (i+1) * 1000) urls.append(URL_TMPL % freq_range) - for i in xrange(0,15): + for i in range(0,15): freq_range = "%d-%d" % (10000 + 2 * i * 1000 + 1, 10000 + (2 * i + 2) * 1000) urls.append(URL_TMPL % freq_range) @@ -53,15 +57,15 @@ def wiki_download(url): tmp_path = DOWNLOAD_TMPL % freq_range if os.path.exists(tmp_path): - print 'cached.......', url + print('cached.......', url) with codecs.open(tmp_path, 'r', 'utf8') as f: return f.read(), True with codecs.open(tmp_path, 'w', 'utf8') as f: - print 'downloading...', url - req = urllib2.Request(url, headers={ + print('downloading...', url) + req = urllib.request.Request(url, headers={ 'User-Agent': 'zxcvbn' }) - response = urllib2.urlopen(req) + response = urllib.request.urlopen(req) result = response.read().decode('utf8') f.write(result) return result, False @@ -173,16 +177,16 @@ def main(): out[lst_name] = lst json.dump(out, f) - print '\nall done! totals:\n' - print 'passwords....', len(passwords) - print 'male.........', len(male_names) - print 'female.......', len(female_names) - print 'surnames.....', len(surnames) - print 'english......', len(english) - print + print('\nall done! totals:\n') + print('passwords....', len(passwords)) + print('male.........', len(male_names)) + print('female.......', len(female_names)) + print('surnames.....', len(surnames)) + print('english......', len(english)) + print() if __name__ == '__main__': if os.path.basename(os.getcwd()) != 'scripts': - print 'run this from the scripts directory' + print('run this from the scripts directory') exit(1) main() diff --git a/zxcvbn/scripts/build_keyboard_adjacency_graph.py b/zxcvbn/scripts/build_keyboard_adjacency_graph.py index 3eca72c..fe700ba 100644 --- a/zxcvbn/scripts/build_keyboard_adjacency_graph.py +++ b/zxcvbn/scripts/build_keyboard_adjacency_graph.py @@ -71,7 +71,7 @@ def build_graph(layout_str, slanted): position_table[(x,y)] = token adjacency_graph = {} - for (x,y), chars in position_table.iteritems(): + for (x,y), chars in position_table.items(): for char in chars: adjacency_graph[char] = [] for coord in adjacency_func(x, y): From e3c6c48decc571b5d737db56123622e682b1f766 Mon Sep 17 00:00:00 2001 From: Alex Willmer Date: Tue, 23 Jun 2015 16:42:36 +0100 Subject: [PATCH 2/4] Add complete .gitignore From https://github.com/github/gitignore/blob/master/Python.gitignore --- .gitignore | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7deee8b..072139d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,61 @@ *~ .*.swp -*.pyc + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ From 970f9b24e28b663216cb4ff66ad66eefa15da95d Mon Sep 17 00:00:00 2001 From: Alex Willmer Date: Tue, 23 Jun 2015 16:46:37 +0100 Subject: [PATCH 3/4] Display command out put in a known order Iteration over a dictionary does not have a defined order. Iterate over a specified list of keys so the output is more predicatable. --- zxcvbn/__init__.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/zxcvbn/__init__.py b/zxcvbn/__init__.py index ad99e97..3c57b57 100644 --- a/zxcvbn/__init__.py +++ b/zxcvbn/__init__.py @@ -8,12 +8,18 @@ if __name__ == '__main__': import fileinput - ignored = ('match_sequence', 'password') + # Ensure fields are displayed in a deterministic order + display_fields = [ + 'crack_time_display', + 'crack_time', + 'score', + 'entropy', + 'calc_time', + ] for line in fileinput.input(): pw = line.strip() print("Password: " + pw) out = password_strength(pw) - for key, value in out.items(): - if key not in ignored: - print("\t%s: %s" % (key, value)) + for key in display_fields: + print("\t%s: %s" % (key, out[key])) From feaacac35e4950ed804e0f2eda2eea21b431e33f Mon Sep 17 00:00:00 2001 From: Alex Willmer Date: Tue, 23 Jun 2015 17:01:14 +0100 Subject: [PATCH 4/4] Remove dependancy on Future, declare 3.x compatibility This commit - Makes python-zxcvbn compatible with Python 2.6, 2.7, 3.2, 3.3 and 3.4 - Removes dependance on the third-party package Future - Switches from distutils -> setuptools; to allow installation as e.g. an egg, a wheel or installing in development mode --- MANIFEST.in | 4 ++++ setup.cfg | 5 +++++ setup.py | 21 ++++++++++++++----- zxcvbn/__init__.py | 4 +++- zxcvbn/main.py | 3 +++ zxcvbn/matching.py | 19 ++++++----------- zxcvbn/scoring.py | 21 +++++++++---------- zxcvbn/scripts/build_frequency_lists.py | 17 ++++++++------- .../scripts/build_keyboard_adjacency_graph.py | 4 +++- 9 files changed, 59 insertions(+), 39 deletions(-) create mode 100644 MANIFEST.in create mode 100644 setup.cfg diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..97f7b6e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,4 @@ +include LICENSE.txt +include tests.txt +include zxcvbn/generated/frequency_lists.json +include zxcvbn/generated/adjacency_graphs.json diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..79bc678 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +# This flag says that the code is written to work on both Python 2 and Python +# 3. If at all possible, it is good practice to do this. If you cannot, you +# will need to generate wheels for each Python version that you support. +universal=1 diff --git a/setup.py b/setup.py index 32b781c..35b4a0c 100644 --- a/setup.py +++ b/setup.py @@ -1,8 +1,5 @@ -from distutils.core import setup +from setuptools import setup -requires = [ - 'future' -] setup(name='zxcvbn', version='1.0', description='Password strength estimator', @@ -10,5 +7,19 @@ author_email='rpearl@dropbox.com', url='https://www.github.com/rpearl/python-zxcvbn', packages=['zxcvbn'], - package_data={'zxcvbn': ['generated/frequency_lists.json', 'generated/adjacency_graphs.json']} + package_data={ + 'zxcvbn': ['generated/frequency_lists.json', + 'generated/adjacency_graphs.json', + ], + }, + classifiers=[ + 'Intended Audience :: Developers', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + ], ) diff --git a/zxcvbn/__init__.py b/zxcvbn/__init__.py index 3c57b57..9c9fe16 100644 --- a/zxcvbn/__init__.py +++ b/zxcvbn/__init__.py @@ -1,4 +1,6 @@ -from __future__ import print_function +from __future__ import (absolute_import, division, + print_function, unicode_literals) + from zxcvbn import main __all__ = ['password_strength'] diff --git a/zxcvbn/main.py b/zxcvbn/main.py index c1709ae..99c80cd 100644 --- a/zxcvbn/main.py +++ b/zxcvbn/main.py @@ -1,3 +1,6 @@ +from __future__ import (absolute_import, division, + print_function, unicode_literals) + import time from zxcvbn.matching import omnimatch diff --git a/zxcvbn/matching.py b/zxcvbn/matching.py index f20de1b..6d3c089 100644 --- a/zxcvbn/matching.py +++ b/zxcvbn/matching.py @@ -1,6 +1,6 @@ -from builtins import map -from builtins import str -from builtins import range +from __future__ import (absolute_import, division, + print_function, unicode_literals) + from itertools import groupby import pkg_resources import re @@ -12,11 +12,6 @@ except ImportError: import json -if (sys.version_info[0] > 2): - PY3=True -else: - PY3=False - GRAPHS = {} DICTIONARY_MATCHERS = [] @@ -73,8 +68,7 @@ def _build_ranked_dict(unranked_list): def _load_frequency_lists(): data = pkg_resources.resource_string(__name__, 'generated/frequency_lists.json') - if PY3: - data = data.decode('utf-8') + data = data.decode('utf-8') dicts = json.loads(data) for name, wordlist in list(dicts.items()): DICTIONARY_MATCHERS.append(_build_dict_matcher(name, _build_ranked_dict(wordlist))) @@ -83,8 +77,7 @@ def _load_frequency_lists(): def _load_adjacency_graphs(): global GRAPHS data = pkg_resources.resource_string(__name__, 'generated/adjacency_graphs.json') - if PY3: - data = data.decode('utf-8') + data = data.decode('utf-8') GRAPHS = json.loads(data) @@ -179,7 +172,7 @@ def dedup(subs): next_subs.append(sub_alternative) subs = dedup(next_subs) keys = rest_keys - return list(map(dict, subs)) + return [dict(sub) for sub in subs] def l33t_match(password): diff --git a/zxcvbn/scoring.py b/zxcvbn/scoring.py index 1a0c8ba..e253980 100644 --- a/zxcvbn/scoring.py +++ b/zxcvbn/scoring.py @@ -1,7 +1,6 @@ -from __future__ import division -from builtins import str -from builtins import range -from past.utils import old_div +from __future__ import (absolute_import, division, + print_function, unicode_literals) + import math import re @@ -127,7 +126,7 @@ def round_to_x_digits(number, digits): """ Returns 'number' rounded to 'digits' digits. """ - return old_div(round(number * math.pow(10, digits)), math.pow(10, digits)) + return round(number * math.pow(10, digits)) / math.pow(10, digits) # ------------------------------------------------------------------------------ # threat model -- stolen hash catastrophe scenario ----------------------------- @@ -147,7 +146,7 @@ def round_to_x_digits(number, digits): SINGLE_GUESS = .010 NUM_ATTACKERS = 100 # number of cores guessing in parallel. -SECONDS_PER_GUESS = old_div(SINGLE_GUESS, NUM_ATTACKERS) +SECONDS_PER_GUESS = SINGLE_GUESS / NUM_ATTACKERS def entropy_to_crack_time(entropy): @@ -334,14 +333,14 @@ def display_time(seconds): if seconds < minute: return 'instant' elif seconds < hour: - return str(1 + math.ceil(old_div(seconds, minute))) + " minutes" + return '%s minutes' % (1 + math.ceil(seconds / minute),) elif seconds < day: - return str(1 + math.ceil(old_div(seconds, hour))) + " hours" + return '%s hours' % (1 + math.ceil(seconds / hour),) elif seconds < month: - return str(1 + math.ceil(old_div(seconds, day))) + " days" + return '%s days' % (1 + math.ceil(seconds / day),) elif seconds < year: - return str(1 + math.ceil(old_div(seconds, month))) + " months" + return '%s months' % (1 + math.ceil(seconds / month),) elif seconds < century: - return str(1 + math.ceil(old_div(seconds, year))) + " years" + return '%s years' % (1 + math.ceil(seconds / year),) else: return 'centuries' diff --git a/zxcvbn/scripts/build_frequency_lists.py b/zxcvbn/scripts/build_frequency_lists.py index 840c6cd..9bea196 100644 --- a/zxcvbn/scripts/build_frequency_lists.py +++ b/zxcvbn/scripts/build_frequency_lists.py @@ -1,8 +1,6 @@ -from __future__ import print_function -from future import standard_library -standard_library.install_aliases() -from builtins import range -from __future__ import with_statement +from __future__ import (absolute_import, division, + print_function, unicode_literals) + import os import time import codecs @@ -12,7 +10,10 @@ except ImportError: import json -import urllib.request, urllib.error, urllib.parse +try: + import urllib.request as urllib_request +except ImportError: + import urllib2 as urllib_request SLEEP_TIME = 20 # seconds @@ -62,10 +63,10 @@ def wiki_download(url): return f.read(), True with codecs.open(tmp_path, 'w', 'utf8') as f: print('downloading...', url) - req = urllib.request.Request(url, headers={ + req = urllib_request.Request(url, headers={ 'User-Agent': 'zxcvbn' }) - response = urllib.request.urlopen(req) + response = urllib_request.urlopen(req) result = response.read().decode('utf8') f.write(result) return result, False diff --git a/zxcvbn/scripts/build_keyboard_adjacency_graph.py b/zxcvbn/scripts/build_keyboard_adjacency_graph.py index fe700ba..47cbfdf 100644 --- a/zxcvbn/scripts/build_keyboard_adjacency_graph.py +++ b/zxcvbn/scripts/build_keyboard_adjacency_graph.py @@ -1,4 +1,6 @@ -from __future__ import with_statement +from __future__ import (absolute_import, division, + print_function, unicode_literals) + try: import simplejson as json json # silence pyflakes