Update libs
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse, math, random
|
||||
|
||||
from pyutil.mathutil import div_ceil
|
||||
|
||||
from pkg_resources import resource_stream
|
||||
|
||||
def recursive_subset_sum(entropy_needed, wordlists):
|
||||
# Pick a minimalish set of numbers which sum to at least
|
||||
# entropy_needed.
|
||||
|
||||
# Okay now what's the smallest number of words which will give us
|
||||
# at least this much entropy?
|
||||
entropy_of_biggest_wordlist = wordlists[-1][0]
|
||||
assert isinstance(entropy_of_biggest_wordlist, float), wordlists[-1]
|
||||
needed_words = div_ceil(entropy_needed, entropy_of_biggest_wordlist)
|
||||
# How much entropy do we need from each word?
|
||||
needed_entropy_per_word = entropy_needed / needed_words
|
||||
# What's the smallest wordlist that offers at least this much
|
||||
# entropy per word?
|
||||
for (wlentropy, wl) in wordlists:
|
||||
if wlentropy >= needed_entropy_per_word:
|
||||
break
|
||||
assert wlentropy >= needed_entropy_per_word, (wlentropy, needed_entropy_per_word)
|
||||
|
||||
result = [(wlentropy, wl)]
|
||||
# If we need more, recurse...
|
||||
if wlentropy < entropy_needed:
|
||||
rest = recursive_subset_sum(entropy_needed - wlentropy, wordlists)
|
||||
result.extend(rest)
|
||||
return result
|
||||
|
||||
def gen_passphrase(entropy, allwords):
|
||||
maxlenwords = []
|
||||
i = 2 # The smallest set is words of length 1 or 2.
|
||||
words = [x for x in allwords if len(x) <= i]
|
||||
maxlenwords.append((math.log(len(words), 2), words))
|
||||
while len(maxlenwords[-1][1]) < len(allwords):
|
||||
i += 1
|
||||
words = [x for x in allwords if len(x) <= i]
|
||||
maxlenwords.append((math.log(len(words), 2), words))
|
||||
|
||||
sr = random.SystemRandom()
|
||||
passphrase = []
|
||||
|
||||
wordlists_to_use = recursive_subset_sum(entropy, maxlenwords)
|
||||
|
||||
passphraseentropy = 0.0
|
||||
for (wle, wl) in wordlists_to_use:
|
||||
passphrase.append(sr.choice(wl))
|
||||
passphraseentropy += wle
|
||||
|
||||
return (u".".join(passphrase), passphraseentropy)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog="chbs", description="Create a random passphrase by picking a few random words.")
|
||||
|
||||
parser.add_argument('-d', '--dictionary', help="what file to read a list of words from (or omit this option to use chbs's bundled dictionary)", type=argparse.FileType('rU'), metavar="DICT")
|
||||
parser.add_argument('bits', help="how many bits of entropy minimum", type=float, metavar="BITS")
|
||||
args = parser.parse_args()
|
||||
|
||||
dicti = args.dictionary
|
||||
if not dicti:
|
||||
dicti = resource_stream('pyutil', 'data/wordlist.txt')
|
||||
allwords = set([x.decode('utf-8').strip().lower() for x in dicti.readlines()])
|
||||
|
||||
passphrase, bits = gen_passphrase(args.bits, allwords)
|
||||
|
||||
print u"Your new password is: '%s'. It is worth about %s bits." % (passphrase, bits)
|
||||
@@ -0,0 +1,209 @@
|
||||
# If you run this file, it will make up a random secret and then crack it
|
||||
# using timing information from a string comparison function. Maybe--if it
|
||||
# gets lucky. It takes a long, long time to work.
|
||||
|
||||
# So, the thing I need help with is statistics. The way this thing works is
|
||||
# extremely stupid. Suppose you want to know which function invocation takes
|
||||
# longer: comparison(secret, guess1) or comparison(secret, guess2)?
|
||||
|
||||
# If you can correctly determine that one of them takes longer than the
|
||||
# other, then (a) you can use that to crack the secret, and (b) this is a
|
||||
# unit test demonstrating that comparison() is not timing-safe.
|
||||
|
||||
# So how does this script do it? Extremely stupidly. First of all, you can't
|
||||
# reliably measure tiny times, so to measure the time that a function takes,
|
||||
# we run that function 10,000 times in a row, measure how long that took, and
|
||||
# divide by 10,000 to estimate how long any one run would have taken.
|
||||
|
||||
# Then, we do that 100 times in a row, and take the fastest of 100 runs. (I
|
||||
# also experimented with taking the mean of 100 runs instead of the fastest.)
|
||||
|
||||
# Then, we just say whichever comparison took longer (for its fastest run of
|
||||
# 100 runs of 10,000 executions per run) is the one we think is a closer
|
||||
# guess to the secret.
|
||||
|
||||
# Now I would *like* to think that there is some kind of statistical analysis
|
||||
# more sophisticated than "take the slowest of the fastest of 100 runs of
|
||||
# 10,000 executions". Such improved statistical analysis would hopefully be
|
||||
# able to answer these two questions:
|
||||
|
||||
# 1. Are these two function calls -- comparison(secret, guess1) and
|
||||
# comparison(secret, guess2) -- drawing from the same distribution or
|
||||
# different? If you can answer that question, then you've answered the
|
||||
# question of whether "comparison" is timing-safe or not.
|
||||
|
||||
# And, this would also allow the cracker to recover from a false step. If it
|
||||
# incorrectly decides the the prefix of the secret is ABCX, when the real
|
||||
# secret is ABCD, then after that every next step it takes will be the
|
||||
# "drawing from the same distribution" kind -- any difference between ABCXQ
|
||||
# and ABCXR will be just due to noise, since both are equally far from the
|
||||
# correct answer, which startsw with ABCD. If it could realize that there is
|
||||
# no real difference between the distributions, then it could back-track and
|
||||
# recover.
|
||||
|
||||
# 2. Giving the ability to measure, noisily, the time taken by comparison(),
|
||||
# how can you most efficiently figure out which guess takes the longest? If
|
||||
# you can do that more efficiently, you can crack secrets more efficiently.
|
||||
|
||||
# The script takes two arguments. The first is how many symbols in the
|
||||
# secret, and the second is how big the alphabet from which the symbols are
|
||||
# drawn. To prove that this script can *ever* work, try passing length 5 and
|
||||
# alphabet size 2. Also try editing the code to let is use sillycomp. That'll
|
||||
# definitely make it work. If you can improve this script (as per the thing
|
||||
# above about "needing better statistics") to the degree that it can crack a
|
||||
# secret with length 32 and alphabet size 256, then that would be awesome.
|
||||
|
||||
# See the result of this commandline:
|
||||
|
||||
# $ python -c 'import time_comparisons ; time_comparisons.print_measurements()'
|
||||
|
||||
|
||||
from pyutil import benchutil
|
||||
|
||||
import hashlib, random, os
|
||||
|
||||
from decimal import Decimal
|
||||
D=Decimal
|
||||
|
||||
p1 = 'a'*32
|
||||
p1a = 'a'*32
|
||||
p2 = 'a'*31+'b' # close, but no cigar
|
||||
p3 = 'b'*32 # different in the first byte
|
||||
|
||||
def randstr(n, alphabetsize):
|
||||
alphabet = [ chr(x) for x in range(alphabetsize) ]
|
||||
return ''.join([random.choice(alphabet) for i in range(n)])
|
||||
|
||||
def compare(n, f, a, b):
|
||||
for i in xrange(n):
|
||||
f(a, b)
|
||||
|
||||
def eqeqcomp(a, b):
|
||||
return a == b
|
||||
|
||||
def sillycomp(a, b):
|
||||
# This exposes a lot of information in its timing about how many leading bytes match.
|
||||
for i in range(len(a)):
|
||||
if a[i] != b[i]:
|
||||
return False
|
||||
for i in xrange(2**9):
|
||||
pass
|
||||
if len(a) == len(b):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def hashcomp(a, b):
|
||||
# Brian Warner invented this for Tahoe-LAFS. It seems like it should be very safe agaist timing leakage of any kind, because of the inclusion of a new random randkey every time. Note that exposing the value of the hash (i.e. the output of md5(randkey+secret)) is *not* a security problem. You can post that on your web site and let all attackers have it, no problem. (Provided that the value of "randkey" remains secret.)
|
||||
|
||||
randkey = os.urandom(32)
|
||||
return hashlib.md5(randkey+ a).digest() == hashlib.md5(randkey+b).digest()
|
||||
|
||||
def xorcomp(a, b):
|
||||
# This appears to be the most popular timing-insensitive string comparison function. I'm not completely sure it is fully timing-insensitive. (There are all sorts of funny things inside Python, such as caching of integer objects < 100...)
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
result = 0
|
||||
for x, y in zip(a, b):
|
||||
result |= ord(x) ^ ord(y)
|
||||
return result == 0
|
||||
|
||||
def print_measurements():
|
||||
N=10**4
|
||||
REPS=10**2
|
||||
|
||||
print "all times are in nanoseconds per comparison (in scientific notation)"
|
||||
print
|
||||
|
||||
for comparator in [eqeqcomp, hashcomp, xorcomp, sillycomp]:
|
||||
print "using comparator ", comparator
|
||||
|
||||
# for (a, b, desc) in [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')]:
|
||||
trials = [(p1, p1a, 'same'), (p1, p2, 'close'), (p1, p3, 'far')]
|
||||
random.shuffle(trials)
|
||||
for (a, b, desc) in trials:
|
||||
print "comparing two strings that are %s to each other" % (desc,)
|
||||
|
||||
def f(n):
|
||||
compare(n, comparator, a, b)
|
||||
|
||||
benchutil.rep_bench(f, N, UNITS_PER_SECOND=10**9, MAXREPS=REPS)
|
||||
|
||||
print
|
||||
|
||||
def try_to_crack_secret(cracker, comparator, secretlen, alphabetsize):
|
||||
secret = randstr(secretlen, alphabetsize)
|
||||
|
||||
def test_guess(x):
|
||||
return comparator(secret, x)
|
||||
|
||||
print "Giving cracker %s a chance to figure out the secret. Don't tell him, but the secret is %s. Whenever he makes a guess, we'll use comparator %s to decide if his guess is right ..." % (cracker, secret.encode('hex'), comparator,)
|
||||
|
||||
guess = cracker(test_guess, secretlen, alphabetsize)
|
||||
|
||||
print "Cracker %s guessed %r" % (cracker, guess,)
|
||||
if guess == secret:
|
||||
print "HE FIGURED IT OUT!? HOW DID HE DO THAT."
|
||||
else:
|
||||
print "HAHA. Our secret is safe."
|
||||
|
||||
def byte_at_a_time_cracker(test_guess, secretlen, alphabetsize):
|
||||
# If we were cleverer, we'd add some backtracking behaviour where, if we can't find any x such that ABCx stands out from the crowd as taking longer than all the other ABCy's, then we start to think that we've taken a wrong step and we go back to trying ABy's. Make sense? But we're not that clever. Once we take a step, we don't backtrack.
|
||||
|
||||
print
|
||||
|
||||
guess=[]
|
||||
|
||||
while len(guess) < secretlen:
|
||||
best_next_byte = None
|
||||
best_next_byte_time = None
|
||||
|
||||
# For each possible byte...
|
||||
for next_byte in range(alphabetsize):
|
||||
c = chr(next_byte)
|
||||
|
||||
# Construct a guess with our best candidate so far...
|
||||
candidate_guess = guess[:]
|
||||
|
||||
# Plus that byte...
|
||||
candidate_guess.append(c)
|
||||
s = ''.join(candidate_guess)
|
||||
|
||||
# Plus random bytes...
|
||||
s += os.urandom(32 - len(s))
|
||||
|
||||
# And see how long it takes the test_guess to consider it...
|
||||
def f(n):
|
||||
for i in xrange(n):
|
||||
test_guess(s)
|
||||
|
||||
times = benchutil.rep_bench(f, 10**7, MAXREPS=10**3, quiet=True)
|
||||
|
||||
fastesttime = times['mean']
|
||||
|
||||
print "%s..."%(c.encode('hex'),),
|
||||
if best_next_byte is None or fastesttime > best_next_byte_time:
|
||||
print "new candidate for slowest next-char: %s, took: %s" % (c.encode('hex'), fastesttime,),
|
||||
|
||||
best_next_byte_time = fastesttime
|
||||
best_next_byte = c
|
||||
|
||||
# Okay we've tried all possible next bytes. Our guess is this one (the one that took longest to be tested by test_guess):
|
||||
guess.append(best_next_byte)
|
||||
print "SLOWEST next-char %s! Current guess at secret: %s" % (best_next_byte.encode('hex'), ''.join(guess).encode('hex'),)
|
||||
|
||||
guess = ''.join(guess)
|
||||
print "Our guess for the secret: %r" % (guess,)
|
||||
return guess
|
||||
|
||||
if __name__ == '__main__':
|
||||
import sys
|
||||
secretlen = int(sys.argv[1])
|
||||
alphabetsize = int(sys.argv[2])
|
||||
if alphabetsize > 256:
|
||||
raise Exception("We assume we can fit one element of the alphabet into a byte.")
|
||||
|
||||
print "secretlen: %d, alphabetsize: %d" % (secretlen, alphabetsize,)
|
||||
|
||||
# try_to_crack_secret(byte_at_a_time_cracker, sillycomp, secretlen, alphabetsize)
|
||||
try_to_crack_secret(byte_at_a_time_cracker, eqeqcomp, secretlen, alphabetsize)
|
||||
Reference in New Issue
Block a user