diff options
Diffstat (limited to 'src')
| -rwxr-xr-x | src/passcheck | 262 | ||||
| -rw-r--r-- | src/passcheck.auto-completion | 8 |
2 files changed, 270 insertions, 0 deletions
diff --git a/src/passcheck b/src/passcheck new file mode 100755 index 0000000..ab3f323 --- /dev/null +++ b/src/passcheck @@ -0,0 +1,262 @@ +#!/usr/bin/env python +# +# passcheck – passphrase strenght evaluator +# +# Copyright © 2013, 2015 Mattias Andrée (maandree@member.fsf.org) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version, or under the terms of the New BSD +# License as published by the Regents of the University of California. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +import sys +import os + + +def _class(char): + char = ord(char) + if ord('0') <= char <= ord('9'): + return 1 + elif ord('a') <= char <= ord('z'): + return 2 + elif ord('A') <= char <= ord('Z'): + return 2.5 + elif char < (1 << 7): + return 3 + elif char < (1 << 8): + return 3.5 + elif char < (1 << 10): + return 4 + elif char < (1 << 14): + return 5 + elif char < (1 << 16): + return 6 + elif char < (1 << 18): + return 7 + elif char < (1 << 22): + return 8 + elif char < (1 << 26): + return 9 + else: + return 10 + + +def distance(a, b): + a, b = a.lower(), b.lower() + if a == b: + return 0 + L1 = '1234567890' + L2 = 'qwertyuiop' + L3 = 'asdfghjkl' + L4 = 'zxcvbnm' + keys = {} + for x in range(len(L1)): + keys[L1[x]] = (x, 0) + for x in range(len(L2)): + keys[L2[x]] = (x + 0.5, 1) + for x in range(len(L3)): + keys[L3[x]] = (x + 0.75, 2) + for x in range(len(L4)): + keys[L4[x]] = (x + 1, 3) + for c in (a, b): + if c not in keys: + return 15 + return ((keys[a][0] - keys[b][0]) ** 2 + (keys[a][1] - keys[b][1]) ** 2) ** 0.5 + + +def search_cmp(haystack, needle): + haystack = haystack + [10] + h, n = 0, 0 + too_low = False + too_high = False + while True: + while True: + hh, nn = haystack[h], needle[n] + if (hh == 10) or (nn == 10): + if hh == nn: + return 0 + break + else: + d = hh - nn + if d != 0: + if d < 0: + too_low = True + break + else: + return None if too_low else 1 + h, n = h + 1, n + 1 + h, n = h + haystack[h:].index(10) + 1, 0 + too_low = too_low or (hh == 10) + too_high = too_high or (nn == 10) + if h == len(haystack): + return None if (too_low and too_high) else (-1 if too_low else 1) + +def pread_full(fd, bs, offset, output): + got_total = 0 + while got_total < bs: + got = list(os.pread(fd, bs - got_total, offset + got_total)) + if len(got) == 0: + break + got_total += len(got) + output.extend(got) + +def search_file(fd, filesize, passphrase): + blocksize = 4096 + minimum = 0 + maximum = filesize - 1 + passphrase = passphrase + [10] + while minimum <= maximum: + middle = (minimum + maximum) // 2 + middle -= middle % blocksize + middle_low = None + continues = 0 + data = [] + while True: + pread_full(fd, blocksize, middle + continues * blocksize, data) + if middle_low is None: + middle_low = 0 + if middle > 0: + try: + middle_low = data.index(10) + except ValueError: + middle_low = None + continue + if middle + len(data) >= filesize: + middle_high = len(data) + else: + middle_high = len(data) - 1 + while (middle_high > middle_low) and (data[middle_high] != 10): + middle_high -= 1 + if middle_high <= middle_low: + continue + if middle > 0: + middle_low += 1 + break + v = search_cmp(data[middle_low : middle_high], passphrase) + if v is None: + return False + elif v < 0: + minimum = middle + middle_low + 1 + elif v > 0: + maximum = middle + middle_high + else: + return True + return False + + +def evaluate(data): + rc = 0 + last = None + data = bytes(data).decode('utf-8', 'replace') + used = {} + classes = [0] * 12 + for c in data: + r = _class(c) + if c not in used: + used[c] = 1 + else: + used[c] += 1 + rc += r ** 2 + rc += 5 / used[c] + if r >= 4: + r += 2 + elif r > 3: + r = 5 + elif r == 3: + r = 4 + elif r > 2: + r = 3 + classes[r - 1] += 1 + if last is not None: + r = distance(c, last) + rc += r ** 0.5 + last = c + if rc >= 0: + rc += 30 + (a, b, c, d) = classes[:4] + if a + b + c + d == 0: + rc += 30 + else: + r = a ** 2 + b ** 2 + c ** 2 + d ** 2 + rc += 30 * len(data) / (r ** 0.5) + return (rc + 0.5) // 1 + + + +waste_ram = ('--waste-ram' in sys.argv[1:]) or ('-w' in sys.argv[1:]) +raw = ('--raw' in sys.argv[1:]) or ('-r' in sys.argv[1:]) + + +blacklist_files = [] +if waste_ram: + try: + with open('blacklist', 'rb') as file: + blacklist = set(file.read().decode('utf-8', 'replace').split('\n')) + except FileNotFoundError: + sys.stderr.write('File "blacklist" from the git branch "large-files" is not present.\n'); + sys.exit(1) +else: + blacklist = set([]) + fd = os.open('blacklist', os.O_RDONLY) + blacklist_files.append((fd, os.fstat(fd).st_size)) +for directory in ['/usr/share/dict/', '/usr/local/share/dict/']: + dictionaries = None + try: + dictionaries = os.listdir(directory) + except FileNotFoundError: + pass + if dictionaries is not None: + for dictionary in dictionaries: + if not os.path.isdir(directory + dictionary): + with open(directory + dictionary, 'rb') as file: + blacklist.update(set(file.read().decode('utf-8', 'replace').split('\n'))) + + +while True: + line = [] + try: + while True: + c = sys.stdin.buffer.read(1)[0] + if c == 10: + break + line.append(c) + except: + break + passphrase = [] + if raw: + passphrase = line + else: + escape = False + for c in line: + if escape: + if (c == ord('~')) or (ord('a') <= c <= ord('z')) or (ord('A') <= c <= ord('Z')): + escape = False + elif c == ord('\033'): + escape = True + else: + passphrase.append(c) + rating = None + if ''.join([chr(c) for c in passphrase]) in blacklist: + rating = 0 + else: + for fd, filesize in blacklist_files: + if search_file(fd, filesize, passphrase): + rating = 0 + break + if rating is None: + rating = evaluate(passphrase) + sys.stdout.buffer.write(('%i \033[34m' % rating).encode('utf-8')) + sys.stdout.buffer.write(bytes(line)) + sys.stdout.buffer.write('\033[00m\n'.encode('utf-8')) + sys.stdout.buffer.flush() + diff --git a/src/passcheck.auto-completion b/src/passcheck.auto-completion new file mode 100644 index 0000000..40e34ae --- /dev/null +++ b/src/passcheck.auto-completion @@ -0,0 +1,8 @@ +(passcheck + (unargumented (options -r --raw) (complete --raw) + (desc 'Treat escape sequences as part of the passphrases')) + + (unargumented (options -w --waste-ram) (complete --waste-ram) + (desc 'Load the data blacklist and create a hashtable')) +) + |
