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/ 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 24af8dc..35b4a0c 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from distutils.core import setup +from setuptools import setup setup(name='zxcvbn', version='1.0', @@ -7,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 7b444a4..9c9fe16 100644 --- a/zxcvbn/__init__.py +++ b/zxcvbn/__init__.py @@ -1,3 +1,6 @@ +from __future__ import (absolute_import, division, + print_function, unicode_literals) + from zxcvbn import main __all__ = ['password_strength'] @@ -7,12 +10,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 + print("Password: " + pw) out = password_strength(pw) - for key, value in out.iteritems(): - if key not in ignored: - print "\t%s: %s" % (key, value) + for key in display_fields: + print("\t%s: %s" % (key, out[key])) 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 a5b31a8..6d3c089 100644 --- a/zxcvbn/matching.py +++ b/zxcvbn/matching.py @@ -1,6 +1,10 @@ +from __future__ import (absolute_import, division, + print_function, unicode_literals) + from itertools import groupby import pkg_resources import re +import sys try: import simplejson as json @@ -29,8 +33,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 +68,16 @@ def _build_ranked_dict(unranked_list): def _load_frequency_lists(): data = pkg_resources.resource_string(__name__, 'generated/frequency_lists.json') + 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') + data = data.decode('utf-8') GRAPHS = json.loads(data) @@ -79,7 +85,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 +128,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 +148,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 +172,7 @@ def dedup(subs): next_subs.append(sub_alternative) subs = dedup(next_subs) keys = rest_keys - return map(dict, subs) + return [dict(sub) for sub in subs] def l33t_match(password): @@ -182,13 +188,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 +204,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 +299,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..e253980 100644 --- a/zxcvbn/scoring.py +++ b/zxcvbn/scoring.py @@ -1,3 +1,6 @@ +from __future__ import (absolute_import, division, + print_function, unicode_literals) + import math import re @@ -253,7 +256,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 +297,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 +333,14 @@ def display_time(seconds): if seconds < minute: return 'instant' elif seconds < hour: - return str(1 + math.ceil(seconds / minute)) + " minutes" + return '%s minutes' % (1 + math.ceil(seconds / minute),) elif seconds < day: - return str(1 + math.ceil(seconds / hour)) + " hours" + return '%s hours' % (1 + math.ceil(seconds / hour),) elif seconds < month: - return str(1 + math.ceil(seconds / day)) + " days" + return '%s days' % (1 + math.ceil(seconds / day),) elif seconds < year: - return str(1 + math.ceil(seconds / month)) + " months" + return '%s months' % (1 + math.ceil(seconds / month),) elif seconds < century: - return str(1 + math.ceil(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 60c61ab..9bea196 100644 --- a/zxcvbn/scripts/build_frequency_lists.py +++ b/zxcvbn/scripts/build_frequency_lists.py @@ -1,4 +1,6 @@ -from __future__ import with_statement +from __future__ import (absolute_import, division, + print_function, unicode_literals) + import os import time import codecs @@ -8,7 +10,10 @@ except ImportError: import json -import urllib2 +try: + import urllib.request as urllib_request +except ImportError: + import urllib2 as urllib_request SLEEP_TIME = 20 # seconds @@ -24,11 +29,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 +58,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 +178,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..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 @@ -71,7 +73,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):