Index: releases/0.0.1/INSTALL
===================================================================
--- releases/0.0.1/INSTALL	(revision 61)
+++ releases/0.0.1/INSTALL	(revision 61)
@@ -0,0 +1,29 @@
+Bletchley Installation Guide
+============================
+
+Bletchley is developed under Debian, but is likely to work on most
+modern Linux distributions, *BSD and MacOS.  Windows is not yet
+supported, but may be in the future.
+
+
+Dependencies
+------------
+* Python 3.x
+* scons (available as a package in most Linux distributions)
+* gcc
+* Optional: python "requests" library (python3-requests under Debian)
+
+
+Installing
+----------
+Build with:
+ $ scons
+
+And as root, install with:
+ # scons install
+
+
+Support
+-------
+Having trouble?  Submit an issue here: 
+  http://code.google.com/p/bletchley/issues/list
Index: releases/0.0.1/LICENSE
===================================================================
--- releases/0.0.1/LICENSE	(revision 61)
+++ releases/0.0.1/LICENSE	(revision 61)
@@ -0,0 +1,165 @@
+                   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions.
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version.
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
Index: releases/0.0.1/SConstruct
===================================================================
--- releases/0.0.1/SConstruct	(revision 61)
+++ releases/0.0.1/SConstruct	(revision 61)
@@ -0,0 +1,93 @@
+import sys
+import os
+sys.dont_write_bytecode = True
+from bletchley_version import BLETCHLEY_VERSION
+
+cflags = '-std=gnu99 -pedantic -Wall -D_FILE_OFFSET_BITS=64 -fvisibility=hidden'
+cflags += ' -DBLETCHLEY_VERSION=\'"%s"\'' % BLETCHLEY_VERSION
+cflags += ' -ggdb'
+
+#lib_src = ['lib/bletchley.c',
+#           'lib/winsec.c',
+#           'lib/range_list.c',
+#           'lib/lru_cache.c',
+#           'lib/void_stack.c']
+
+cc=os.environ.get('CC', 'gcc')
+env = Environment(ENV=os.environ,
+                  CC=cc,
+                  CFLAGS=cflags,
+                  CPPPATH=['include', '/usr/local/include'],
+                  LIBPATH=['lib', '/usr/local/lib'],
+                  LIBS=[])
+
+# Libraries
+#libbletchley_static = env.Library(lib_src)
+#libbletchley = env.SharedLibrary(lib_src, LIBS=['m','pthread', 'talloc'], 
+#                                 LINKFLAGS="-shared -fPIC -Wl,-soname,libbletchley.so.%s" % BLETCHLEY_VERSION)
+
+
+# Executable binaries
+bletchley_nextrand = env.Program('bin/bletchley-nextrand', ['src/nextrand.c'])
+
+
+# Documentation
+#  This only needs to be run during the release/packaging process
+#man_fixup = "|sed 's/.SH DESCRIPTION/\\n.SH DESCRIPTION/'"
+#man_builder = Builder(action='docbook2x-man --to-stdout $SOURCE'
+#                      + man_fixup + '| gzip -9 > $TARGET',
+#                      suffix = '.gz',
+#                      src_suffix = '.docbook')
+#env['BUILDERS']['ManPage'] = man_builder
+
+#man_bletchley = env.ManPage('doc/bletchley.1.docbook')
+#man_bletchley_recover = env.ManPage('doc/bletchley-recover.1.docbook')
+#man_bletchley_timeline = env.ManPage('doc/bletchley-timeline.1.docbook')
+
+# Installation
+prefix     = os.environ.get('PREFIX','/usr/local')+'/'
+destdir    = os.environ.get('DESTDIR','')
+bindir     = os.environ.get('BINDIR', prefix + 'bin')
+libdir     = os.environ.get('LIBDIR', prefix + 'lib')
+includedir = os.environ.get('INCLUDEDIR', prefix + 'include')
+mandir     = os.environ.get('MANDIR', prefix + 'man')
+
+install_items = [destdir + bindir]
+
+env.Install(destdir+bindir, [bletchley_nextrand, 'bin/bletchley-analyze', 
+                             'bin/bletchley-http2py', 'bin/bletchley-encode', 
+                             'bin/bletchley-decode'])
+#libinstall = env.Install(destdir+libdir, [libbletchley, libbletchley_static])
+#env.Install(destdir+includedir+'/bletchley', Glob('include/*.h'))
+#env.Install(destdir+mandir+'/man1', [man_bletchley, man_bletchley_recover,
+#                                     man_bletchley_timeline])
+#if os.getuid() == 0:
+#   env.AddPostAction(libinstall, 'ldconfig')
+
+#if sys.version_info[0] == 2:
+#   install_items.append('bletchley-python2.log')
+#   env.Command('bletchley-python2.log', Glob('lib/bletchley/*.py')+Glob('lib/bletchley/CBC/*.py'),
+#               "python bletchley-distutils install --root=/%s | tee bletchley-python2.log" % destdir)
+
+python_path = os.popen('which python3').read()
+if python_path != '':
+   install_items.append('bletchley-python3.log')
+   env.Command('bletchley-python3.log', Glob('lib/bletchley/*.py')+Glob('lib/bletchley/CBC/*.py'),
+               "python3 bletchley-distutils install --root=/%s | tee bletchley-python3.log" % destdir)
+
+# API documentation
+#bletchley_doc = env.Command('doc/devel/bletchley/index.html', 
+#                            Glob('lib/*.c')+Glob('include/*.h')+['doc/devel/Doxyfile.bletchley'],
+#                            'doxygen doc/devel/Doxyfile.bletchley')
+#pybletchley_doc = env.Command('doc/devel/pybletchley/index.html', 
+#                              Glob('python/pybletchley/*.py')+['doc/devel/Doxyfile.pybletchley', bletchley_doc],
+#                              'doxygen doc/devel/Doxyfile.pybletchley')
+
+
+# User Friendly Targets
+env.Alias('bin', [bletchley_nextrand])
+#env.Alias('doc', [man_bletchley,man_bletchley_recover,man_bletchley_timeline])
+#env.Alias('doc-devel', [bletchley_doc, pybletchley_doc])
+env.Alias('install', install_items)
+
+Default('bin')
Index: releases/0.0.1/bin/bletchley-analyze
===================================================================
--- releases/0.0.1/bin/bletchley-analyze	(revision 61)
+++ releases/0.0.1/bin/bletchley-analyze	(revision 61)
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+
+# Requires Python 3+
+
+'''
+Simple script to leverage some of the blobtools features
+
+Copyright (C) 2011-2012 Virtual Security Research, LLC
+Author: Timothy D. Morgan
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+
+import sys
+import argparse
+import binascii
+from bletchley import blobtools,buffertools
+
+
+parser = argparse.ArgumentParser(
+    description='Analyzes samples of encrypted data in an attempt to decode'
+    ' samples to binary and identify patterns useful in cryptanalysis.'
+    ' For more information, see:  http://code.google.com/p/bletchley/wiki/Overview')
+parser.add_argument('input_file', nargs='?', default=None,
+                    help='File containing encrypted blobs to analyze, one per'
+                    ' line. Omit to read from stdin.')
+parser.add_argument('-e', dest='encoding_chain', type=str, default=None,
+                    help='Comma-separated sequence of formats used to encode'
+                    ' the tokens, beginning with the last one applied. '
+                    '(default: auto-detect. Use "?" for a listing of supported encodings.)')
+parser.add_argument('-b', dest='block_size', type=int, default=8,
+                    help='The block size displayed and used in highlighting (default: 8)')
+parser.add_argument('-p', dest='output_lines', type=int, default=10,
+                    help='Number of lines of input to display in decoded form.')
+parser.add_argument('-c', action='store_const', const=True, default=False,
+                    help='Use color output for decoded highlights.')
+parser.add_argument('-c256', action='store_const', const=True, default=False,
+                    help='Use 256 color output for decoded highlights.')
+options = parser.parse_args()
+
+if options.encoding_chain == '?':
+    print('\n\t'.join(['Supported encodings:']+blobtools.supportedEncodings()))
+    sys.exit(0)
+
+input_file = sys.stdin.buffer
+if options.input_file is not None:
+    input_file = open(options.input_file, 'rb')
+
+blobs = input_file.read().rstrip(b'\n').replace(b'\r', b'').split(b'\n')
+#print(repr(blobs))
+
+def terminalHighlightedString(code, s):
+    global options
+    if code == None:
+        return s
+
+    if options.c256:
+        fg = code%216+16
+        bg = (code^0xffffffff)%216+16
+        return "\x1b[0;38;05;%d;48;05;%dm%s\x1b[0m" % (fg,bg,s)
+    elif options.c:
+        fg = code%6+31
+        bg = (code^0xffffffff)%6+41
+        return "\x1b[0;%d;%dm%s\x1b[0m" % (fg,bg,s)
+    else:
+        return "\x1b[7m%s\x1b[0m" % s
+
+
+def printColoredHexDump(blobs, group_size=8):    
+    line_size = 64
+    group_size *= 2 # two hex digits per byte
+    hex_blobs = [binascii.b2a_hex(b) for b in blobs]
+    color_map = buffertools.blockWiseColorMap(group_size, hex_blobs)
+
+    for k in range(0,len(hex_blobs)):
+        hex = hex_blobs[k].decode()
+        
+        for i in range(0,len(hex),line_size):
+            line = '%.4X: ' % i
+
+            if len(hex[i:]) < line_size:
+                hex += (line_size-len(hex[i:]))*' '
+
+            for j in range(0,line_size,group_size):
+                group = hex[i+j:i+j+group_size]
+                line += terminalHighlightedString(color_map.get(group.encode('utf-8'),
+                                                                None), group) + ' ' 
+       
+            line += '| '
+            line += repr(blobs[k][i//2:(i+line_size)//2])
+        
+            print(line)
+        print('')
+
+
+
+def analyze(blobs):
+    lengths = blobtools.getLengths(blobs)
+    print('Unique Lengths: ' + ','.join(map(str, lengths)))
+
+    max_block_size = blobtools.maxBlockSize(lengths)
+    print('Maximum Possible Block Size: %d' % max_block_size)
+
+    block_sizes = blobtools.checkCommonBlocksizes(lengths)
+    print('Matching Common Block Sizes: %s' % ','.join(map(str, block_sizes)))
+
+    encodings = blobtools.encodingIntersection(blobs)
+    print('Possible Encodings: ' + ','.join(encodings))
+
+    encoding = blobtools.bestEncoding(encodings)
+    print('Best Encoding: %s' % encoding)
+
+    return encoding,block_sizes
+
+specified_encodings = []
+if options.encoding_chain != None:
+    specified_encodings = options.encoding_chain.split(',')
+
+encoding_chain = []
+encoding = 'dummy'
+while encoding != None:
+    print('='*80)
+    print('Beginning analysis after decoding by chain: %s' % ','.join(encoding_chain))
+    encoding,block_sizes = analyze(blobs)
+    if len(specified_encodings) > 0:
+        encoding = specified_encodings.pop(0)
+
+    blobs_to_print = blobs[:options.output_lines]
+    print('First %d Values:' % len(blobs_to_print))
+
+    printColoredHexDump(blobs_to_print, options.block_size)
+
+    if encoding != None:
+        encoding_chain.append(encoding)
+        blobs = blobtools.decodeAll(encoding, blobs)
+
Index: releases/0.0.1/bin/bletchley-decode
===================================================================
--- releases/0.0.1/bin/bletchley-decode	(revision 61)
+++ releases/0.0.1/bin/bletchley-decode	(revision 61)
@@ -0,0 +1,54 @@
+#!/usr/bin/env python3
+
+# Requires Python 3+
+
+'''
+Simple decoder script; useful in shell scripts
+
+Copyright (C) 2011-2012 Virtual Security Research, LLC
+Author: Timothy D. Morgan
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+
+import sys
+import argparse
+from bletchley import blobtools
+
+
+parser = argparse.ArgumentParser(
+    description='A simple decoder script that is useful in shell scripts.'
+    ' For more information, see:  http://code.google.com/p/bletchley/wiki/Overview')
+parser.add_argument(
+    'input_file', nargs='?', default=None,
+    help='File containing encrypted blobs to analyze, one per line.'
+    ' Omit to read from stdin.')
+parser.add_argument(
+    '-e', dest='encoding_chain', type=str, required=True,
+    help='Comma-separated sequence of encoding formats used to encode the token.'
+    ' (Use "?" for a listing of supported encodings.)')
+options = parser.parse_args()
+
+if options.encoding_chain == '?':
+    print('\n\t'.join(['Supported encodings:']+blobtools.supportedEncodings()))
+    sys.exit(0)
+
+input_file = sys.stdin.buffer
+if options.input_file is not None:
+    input_file = open(options.input_file, 'rb')
+
+blob = input_file.read().rstrip(b'\r\n')
+
+specified_encodings = options.encoding_chain.split(',')
+sys.stdout.buffer.write(blobtools.decodeChain(specified_encodings, blob))
Index: releases/0.0.1/bin/bletchley-encode
===================================================================
--- releases/0.0.1/bin/bletchley-encode	(revision 61)
+++ releases/0.0.1/bin/bletchley-encode	(revision 61)
@@ -0,0 +1,58 @@
+#!/usr/bin/env python3
+
+# Requires Python 3+
+
+'''
+Simple decoder script; useful in shell scripts
+
+Copyright (C) 2011-2012 Virtual Security Research, LLC
+Author: Timothy D. Morgan
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+
+import sys
+import argparse
+from bletchley import blobtools
+
+
+parser = argparse.ArgumentParser(
+    description='A simple encoder script that is useful in shell scripts.'
+    ' For more information, see:  http://code.google.com/p/bletchley/wiki/Overview')
+parser.add_argument(
+    'input_file', nargs='?', default=None,
+    help='File containing encrypted blobs to analyze, one per line. '
+    'Omit to read from stdin.')
+parser.add_argument(
+    '-e', dest='encoding_chain', type=str, required=True,
+    help='Comma-separated sequence of encoding formats used to encode the'
+    ' token, starting with the last encoding to apply.'
+    ' (Use "?" for a listing of supported encodings.)')
+options = parser.parse_args()
+
+if options.encoding_chain == '?':
+    print('\n\t'.join(['Supported encodings:']+blobtools.supportedEncodings()))
+    sys.exit(0)
+
+input_file = sys.stdin.buffer
+if options.input_file is not None:
+    input_file = file(options.input_file, 'rb')
+
+blob = input_file.read()
+
+specified_encodings = options.encoding_chain.split(',')
+specified_encodings.reverse()
+# XXX: report invalid encodings
+sys.stdout.buffer.write(blobtools.encodeChain(specified_encodings, blob))
+sys.stdout.buffer.write(b'\n')
Index: releases/0.0.1/bin/bletchley-http2py
===================================================================
--- releases/0.0.1/bin/bletchley-http2py	(revision 61)
+++ releases/0.0.1/bin/bletchley-http2py	(revision 61)
@@ -0,0 +1,235 @@
+#!/usr/bin/env python3
+
+# Requires Python 3+
+
+'''
+This script reads a raw HTTP request and writes to stdout a Python
+script.  The generated script sends the same (or a very similar) 
+request using the standard httplib/http.client library, or optionally
+using the more user friendly python-requests library.
+
+Certainly if you have a raw request, you could simply send it via TCP
+sockets, but if for some reason the server behaves oddly with flow control,
+insists on using gzip/deflate encoding, insists on using chunked encoding,
+or any number of other annoying things, then using an HTTP library is a 
+lot more convenient.  This script attempts to make that conversion easy.
+
+
+Copyright (C) 2011-2013 Virtual Security Research, LLC
+Author: Timothy D. Morgan
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+
+import sys
+import argparse
+
+bopen = lambda f: open(f, 'rb')
+
+parser = argparse.ArgumentParser(
+    description='A script which accepts an HTTP request and prints out a'
+    ' generated Python script which sends a similar request.  This is useful'
+    ' when one wants to automate sending a large number of requests to a'
+    ' particular page or application.'
+    ' For more information, see: http://code.google.com/p/bletchley/wiki/Overview')
+parser.add_argument(
+    'requestfile', type=bopen, nargs='?', default=sys.stdin.buffer, 
+    help='A file containing an HTTP request.  Defaults to stdin if omitted.')
+parser.add_argument(
+    '--requests', action='store_true', help='Generate a script that uses the'
+    ' python-requests module rather than httplib/http.client (experimental).')
+
+args = parser.parse_args()
+input_req = args.requestfile.read()
+
+
+if b'\r\n\r\n' in input_req:
+    raw_headers,body = input_req.split(b'\r\n\r\n', 1)
+elif b'\n\n' in input_req:
+    raw_headers,body = input_req.split(b'\n\n', 1)
+else:
+    raw_headers = input_req
+    body = b''
+
+raw_headers = raw_headers.decode('utf-8')
+
+header_lines = raw_headers.split('\n')
+method,path,version = header_lines[0].split(' ', 2)
+
+host = 'TODO'
+port = 80
+use_ssl = False
+protocol = 'http'
+
+headers = []
+for l in header_lines[1:]:
+    if len(l) < 1: 
+        break
+    # Handle header line continuations
+    if l[0] in ' \t':
+        if len(headers) == 0:
+            continue
+        name,values = headers[-1]
+        values.append(l.lstrip('\t'))
+        headers[-1] = (name,values)
+        continue
+
+    name,value = l.split(':',1)
+    value = value.lstrip(' ').rstrip('\r')
+
+    # Skip headers that have to do with transfer encodings and connection longevity
+    if name.lower() not in ['accept','accept-language',
+                            'accept-encoding','accept-charset',
+                            'connection', 'keep-alive', 'host', 
+                            'content-length', 'proxy-connection']:
+        headers.append((name,[value]))
+
+    if name.lower() == 'host':
+        if ':' in value:
+            host,port = value.split(':',1)
+            port = int(port, 10)
+            if port == 443:
+                use_ssl = True
+                protocol = 'https'
+        else:
+            host = value
+
+
+formatted_body = '\n            '.join([repr(body[i:i+40]) for i in range(0,len(body),40)])
+if formatted_body == '':
+    formatted_body = "b''"
+
+
+if args.requests:
+    print('''#!/usr/bin/env python3
+
+import sys
+try:
+    import requests
+except:
+    sys.stderr.write('ERROR: Could not import requests module.  Ensure it is installed.\\n')
+    sys.stderr.write('       Under Debian, the package name is "python3-requests"\\n.')
+    sys.exit(1)
+
+
+# TODO: ensure the host, port, and SSL settings are correct.
+host = %s
+port = %s
+protocol = %s
+''' % (repr(host),repr(port),repr(protocol)))
+
+    headers = dict(headers)
+    # XXX: We don't currently support exactly formatted header
+    #      continuations with python requests, but this should be
+    #      semantically equivalent.
+    for h in headers.keys():
+        headers[h] = ' '.join(headers[h])
+
+    print('''
+session = requests.Session()
+# TODO: use "data" to supply any parameters to be included in the request
+def sendRequest(session, data=None):
+    method = %s
+    path = %s
+    headers = %s
+    url = "%%s://%%s:%%d%%s" %% (protocol,host,port,path)
+    body = (%s)
+
+    return session.request(method, url, headers=headers, data=body)
+    ''' % (repr(method), repr(path), repr(headers), formatted_body))
+
+    print('''    
+
+def fetch(data):
+    global session
+    ret_val = None
+
+    # TODO: customize code here to retrieve what you need from the response(s)
+    # For information on the response object's interface, see:
+    #   http://docs.python-requests.org/en/latest/api/#requests.Response
+    response = sendRequest(session, data)
+    print(response.headers)
+    print(repr(response.content))
+
+    return ret_val
+
+data = ''
+fetch(data)
+''')
+
+
+
+else:
+    print('''#!/usr/bin/env python3
+
+import sys
+import http.client as httpc
+
+
+# TODO: ensure the host, port, and SSL settings are correct.
+host = %s
+port = %s
+use_ssl = %s
+''' % (repr(host),repr(port),repr(use_ssl)))
+
+    print('''
+# TODO: use "data" to supply any parameters to be included in the request
+def sendRequest(connection, data=None):
+    method = %s
+    path = %s
+    body = (%s)
+    
+    connection.putrequest(method, path)
+    ''' % (repr(method), repr(path), formatted_body))
+
+    for name,values in headers:
+        if len(values) > 1:
+            continuations = ','.join([repr(v) for v in values[1:]])
+            print('''    connection.putheader(%s, %s, %s)''' % (repr(name),repr(values[0]),continuations))
+        else:
+            print('''    connection.putheader(%s, %s)''' % (repr(name),repr(values[0])))
+
+    print('''    
+    if len(body) > 0:
+        connection.putheader('Content-Length', len(body))
+    connection.endheaders()
+    connection.send(body)
+    
+    return connection.getresponse()
+
+
+def newConnection():
+    if use_ssl:
+        return httpc.HTTPSConnection(host, port)
+    else:
+        return httpc.HTTPConnection(host, port)
+
+
+def fetch(data):
+    ret_val = None
+    connection = newConnection()
+
+    # TODO: customize code here to retrieve what you need from the response(s)
+    # For information on the response object's interface, see:
+    #   http://docs.python.org/library/httplib.html#httpresponse-objects
+    response = sendRequest(connection, data)
+    print(response.getheaders())
+    print(repr(response.read()))
+
+    connection.close()
+    return ret_val
+
+data = ''
+fetch(data)
+''')
Index: releases/0.0.1/bletchley-distutils
===================================================================
--- releases/0.0.1/bletchley-distutils	(revision 61)
+++ releases/0.0.1/bletchley-distutils	(revision 61)
@@ -0,0 +1,8 @@
+# Called from scons with appropriate python version
+import sys
+from distutils.core import setup
+sys.dont_write_bytecode = True
+from bletchley_version import BLETCHLEY_VERSION
+sys.dont_write_bytecode = False
+
+setup(name='bletchley', version=BLETCHLEY_VERSION, package_dir={'':'lib'}, packages=['bletchley', 'bletchley.CBC'])
Index: releases/0.0.1/bletchley_version.py
===================================================================
--- releases/0.0.1/bletchley_version.py	(revision 61)
+++ releases/0.0.1/bletchley_version.py	(revision 61)
@@ -0,0 +1,2 @@
+SVN_REV = "$Rev$"
+BLETCHLEY_VERSION = "0.0.%s" % SVN_REV.split(' ')[1]
Index: releases/0.0.1/doc/TODO
===================================================================
--- releases/0.0.1/doc/TODO	(revision 61)
+++ releases/0.0.1/doc/TODO	(revision 61)
@@ -0,0 +1,40 @@
+Near Term
+=========
+
+* Ciphertext block brute-force classes
+
+* A stream ciphertext probe tool to build a map of different error messages
+
+* A tool that behaves like http2py, but generates a POA script
+
+
+
+Future
+======
+
+* Generalize CBC-R methods to support any block decryption oracle
+  http://blog.spiderlabs.com/2013/06/cbc-r-its-not-just-for-padding-oracles.html
+
+* Codetective
+  Consider using parts of this, if useful
+  https://github.com/blackthorne/Codetective
+
+* nextrand
+  - Expand capabilities to handle nextInt calls with arguments
+  - Transition code to a library that could be called by Python
+
+* Consider using an SMT solver for certain problems (linear PRNGs?)
+  http://en.wikipedia.org/wiki/Satisfiability_Modulo_Theories
+
+* Add tools for testing and conducting hash length-extension attacks.
+  One tool: https://github.com/bwall/HashPump
+
+* Add tool to test for LCG without knowing LCG parameters
+  See: Inferring Sequences Produced by Pseudo-Random Number Generators by JOAN BOYAR
+
+* Add Mersenne Twister attack tool
+  See: http://seclists.org/fulldisclosure/2012/Oct/190
+
+* Add tools for helping one determine the character set used by
+  randomly generated passwords, as well as determining the mapping
+  from numbers to characters
Index: releases/0.0.1/lib/bletchley/CBC/Exceptions.py
===================================================================
--- releases/0.0.1/lib/bletchley/CBC/Exceptions.py	(revision 61)
+++ releases/0.0.1/lib/bletchley/CBC/Exceptions.py	(revision 61)
@@ -0,0 +1,29 @@
+'''
+Created on Jul 4, 2010
+
+Copyright (C) 2010 ELOI SANFÈLIX
+@author: Eloi Sanfelix < eloi AT limited-entropy.com >
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+class InvalidBlockError(Exception):
+    '''
+    classdocs
+    '''
+    def __init__(self, expectedSize, receivedSize):
+        self.expected = expectedSize
+        self.received = receivedSize
+    def __str__(self):
+        return "Invalid block size: "+self.received+" bytes. Block must be "+self.expected+" bytes long."
+
Index: releases/0.0.1/lib/bletchley/CBC/TimingWebPaddingOracle.py
===================================================================
--- releases/0.0.1/lib/bletchley/CBC/TimingWebPaddingOracle.py	(revision 61)
+++ releases/0.0.1/lib/bletchley/CBC/TimingWebPaddingOracle.py	(revision 61)
@@ -0,0 +1,156 @@
+'''
+Created on Sep 21, 2010
+
+Copyright (C) 2010 ELOI SANFÈLIX
+@author: Eloi Sanfelix < eloi AT limited-entropy.com >
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+from time import *
+from urllib import urlencode
+from urllib2 import *
+import struct
+import urllib2
+
+class TimingWebPaddingOracle:
+    
+    def __init__(self, url , encoder = None, decoder = None, requests=100,headers = {}):
+        #Initialize Request with URL and headers
+        #self.req = Request(url,headers)
+        self.url = url
+        self.headers = headers
+        self.data = {}
+        self.oracle_name = ""
+        self.oracle_value = ""
+        self.time_threshold = None
+        self.requests = requests
+        
+        if(encoder== None and decoder!=None) or (encoder!=None and decoder==None):
+            print("ERROR: Encoder and decoder must be both set or not set at all. Disabling both.")
+            self.encoder = None
+            self.decoder = None
+        else:
+            self.encoder = encoder
+            self.decoder = decoder
+        
+    def add_variable(self,name,value, oracle=False):
+        if(oracle):
+            #Data is defined as being vulnerable to oracle attack.
+            self.oracle_name = name
+            self.oracle_value = value
+        else:
+            #Add to dictionary with data
+            self.data[name] = value
+    
+    def analyze_normal_request(self):
+        
+        newdict = self.data.copy()
+        newdict[self.oracle_name] = self.oracle_value
+        r = urlencode(newdict)
+        #r = ""
+        #for i in self.data:
+        #    r = r + str(i)+"="+urlencode(self.data[i])+"&"
+        #r = r + str(self.oracle_name) + "=" + urlencode(self.oracle_value)
+        return self.analyze_request(r)
+    
+    def analyze_request(self,data):
+        t = 0
+        for i in range(self.requests):
+            t += self.perform_request(data)
+        return t/self.requests
+    
+    def perform_request(self,r):
+        t = time.time()
+        req = Request(self.url,self.headers)
+        req.add_data(r)
+        f = urllib2.urlopen(req)
+        f.read() # Read result from page
+        t = time.time() - t
+        return t
+        
+    def test_oracle(self):
+        if(self.oracle_name == None or self.oracle_value == None):
+            print("ERROR: Cannot test_oracle if no oracle variable defined")
+            return
+        
+        #Perform 'normal' analysis first
+        time1 = self.analyze_normal_request()            
+        
+        #Decode value if needed
+        if(self.decoder != None):
+            value = self.decoder(self.oracle_value)
+        else:
+            value = self.oracle_value
+        
+        oracle_list = [struct.unpack("B", value[i])[0] for i in range(len(value))]
+        
+        oracle_list[-1] ^= 0xFF #Ensure we always start with a different value
+
+        for b in range(256):
+            # XOR current counter. Last byte is now b^0xFF^i = ~b ^ i
+            oracle_list[-1] ^=b
+            v = "".join([struct.pack("B", i) for i in oracle_list])
+            if (self.encoder != None):
+                v = self.encoder(v)
+            
+            newdict = self.data.copy()
+            newdict[self.oracle_name] = v
+            r = urlencode(newdict)
+            
+            #And return to original value
+            oracle_list[-1] ^=b
+            
+            time2 = self.analyze_request(r)
+            
+            #FIXME most likely this will find a difference
+            if(time1 != time2):
+                print("Found difference for i="+hex(b))
+                print("Original timing: " + str(time1))
+                print("Bad timing: " + str(time2))
+                self.time_threshold = abs(time1 - time2) / 2 + min(time1,time2)
+                if(time1 > time2):
+                    self.oracle_type = 0x01 #Normal timing is higher than threshold
+                else:
+                    self.oracle_type = 0x02 #Normal timing is lower than threshold
+                return True
+        print("ERROR: Could not find a difference.")
+        return False
+    
+    def oracle(self,ctext):
+        if(self.time_threshold == None):
+            print("ERROR: Oracle not defined!")
+        else:
+            
+            newdict = self.data.copy()
+            if(self.encoder != None):
+                ctext = self.encoder(ctext) #Encode ciphertext before sending request if needed
+            newdict[self.oracle_name] = ctext
+            
+            r = urlencode(newdict)
+            t = self.analyze_request(r)
+            
+            if (self.oracle_type == 0x01 ):
+                ret = self.time_threshold < t #Padding is correct if time above threshold
+            else:
+                ret = self.time_threshold > t #Padding is incorrect if time above threshold
+            return ret
+        
+    def set_threshold(self,threshold):
+        self.time_threshold = threshold
+    
+    def set_type(self,type):
+        self.oracle_type = type #0x01 means correct padding takes more time    
+    
+    def hex_string(self,data):
+        return "".join([ hex(ord(i))+" " for i in data])
Index: releases/0.0.1/lib/bletchley/CBC/__init__.py
===================================================================
--- releases/0.0.1/lib/bletchley/CBC/__init__.py	(revision 61)
+++ releases/0.0.1/lib/bletchley/CBC/__init__.py	(revision 61)
@@ -0,0 +1,394 @@
+'''
+Created on Jul 4, 2010
+
+Copyright (C) 2010 ELOI SANFÈLIX
+Copyright (C) 2012-2013 Timothy D. Morgan
+@author: Eloi Sanfelix < eloi AT limited-entropy.com >
+@author: Timothy D. Morgan < tmorgan {a} vsecurity . com >
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import random
+import struct
+import threading
+from .. import buffertools
+from .Exceptions import *
+
+class POA:
+    """This class implements padding oracle attacks given a ciphertext and
+    function that acts as a padding oracle.
+
+    The padding scheme is assumed to be PKCS#5/#7, also defined in RFC2040.
+    This attack was first described in:
+     "Security Flaws Induced by CBC Padding. Applications to SSL, IPSEC,
+      WTLS" by Serge Vaudenay (2002)
+
+    POA objects are not caller thread-safe.  If multiple threads need to work
+    simultaneously on the same ciphertext and oracle, create a
+    separate instance. POA objects can execute tasks internally using
+    multiple threads, however.
+
+    """
+
+    ## private
+    _thread_result = None
+    _oracle = None
+    _ciphertext = None
+    _iv = None
+
+    ## protected (reading ok, changing not ok)
+    block_size = None
+
+    ## public (r/w ok)
+    retries = 2
+    decrypted = None
+    threads = None
+    log_fh = None
+    
+    def __init__(self, oracle, block_size, ciphertext, iv=None,
+                 threads=1, decrypted=b'', log_file=None):
+        """Creates a new padding oracle attack (POA) object. 
+
+        Arguments:
+        oracle -- A function which returns True if the given ciphertext
+         results in a correct padding upon decryption and False
+         otherwise.  This function should implement the prototype:
+           def myOracle(ciphertext, iv): ...
+         If the initialization vector (iv) is unknown is not included in
+         the ciphertext message, it can be ignored in the oracle
+         implementation (though some limitations will result from this).
+
+        block_size -- The block size of the ciphertext being attacked.
+         Is almost always 8 or 16.
+
+        ciphertext -- The ciphertext to be decrypted
+
+        iv -- The initialization vector associated with the ciphertext.
+         If none provided, it is assumed to be a block of 0's
+
+        threads -- The maximum number of parallel threads to use during
+         decryption.  If more than one thread is used, then the oracle
+         function will be called in parallel.  It should implement any
+         internal locking necessary to prevent race conditions where
+         applicable.
+
+        decrypted -- If a portion of the plaintext is already known (due
+         to a prior, partially successful decryption attempt), then this
+         may be used to restart the decryption process where it was
+         previously left off.  This argument is assumed to contain the
+         final N bytes (for an N-byte argument) of the plaintext; that
+         is, the tail of the plaintext including the pad.
+
+        log_file -- A Python file object where log messages will be
+         written.
+
+        """
+
+        if(len(ciphertext)%block_size != 0 or len(ciphertext) < block_size):
+            raise InvalidBlockError(block_size,len(ciphertext))
+        if(iv != None and len(iv)%block_size != 0):
+            raise InvalidBlockError(block_size,len(iv))
+        if len(decrypted) > len(ciphertext):
+            raise Exception #XXX: custom exception
+        
+        self.block_size = block_size
+        self.decrypted = decrypted
+        self.threads = threads
+        self.log_fh = log_file
+
+        self._oracle = oracle
+        self._ciphertext = ciphertext
+        if iv == None:
+            self._iv = b'\x00'*self.block_size
+        else:
+            self._iv = iv
+
+
+    def log_message(self, s):
+        if self.log_fh != None:
+            self.log_fh.write(s+'\n')
+
+
+    def probe_padding(self):
+        """Attempts to verify that a CBC padding oracle exists and then determines the
+        pad value.  
+
+        Returns the pad string, or None on failure. 
+        XXX: Currently only works for PKCS 5/7.
+        """
+
+        blocks = buffertools.splitBuffer(self._ciphertext, self.block_size)
+        final = blocks[-1]
+        if len(blocks) == 1:
+            # If only one block present, then try to use IV as prior
+            prior = self._iv
+        else:
+            prior = blocks[-2]
+
+        ret_val = None
+        # First probe for beginning of pad
+        for i in range(0-self.block_size,0):
+            if i == -1:
+                break
+            tweaked = prior[i] ^ 0xFF
+            tweaked = struct.pack("B", tweaked)
+            if not self._oracle(self._ciphertext+prior[:i]+tweaked+prior[i+1:]+final, self._iv):
+                break
+
+        pad_length = 0-i
+        self.log_message("Testing suspected pad length: %d" % pad_length)
+        if pad_length > 1:
+            # XXX: If this test case fails, we should try instead
+            # lengthing the pad by one byte with all 256 values (as is
+            # done in the 1-byte pad case).
+            #
+            # Verify suspected pad length by changing last pad byte to 1
+            # and making sure the padding succeeds
+            tweaked = prior[-1] ^ (pad_length^1)
+            tweaked = struct.pack("B", tweaked)
+
+            #XXX: This replaces the pad bytes with spaces.  The hope is
+            #     that any UTF-8 decoding errors that the pad bytes
+            #     might generate are addressed this way.  It is not yet
+            #     well tested.  An option should be added to allow other
+            #     bytes to be used or to turn off the behavior.
+            prior = bytearray(prior)
+            for q in range(0-self.block_size,-1):
+                prior[q] = prior[q]^(pad_length^32) # space
+
+            if self._oracle(self._ciphertext+prior[:-1]+tweaked+final, self._iv):
+                ret_val = buffertools.pkcs7Pad(pad_length)
+
+        else:
+            # Verify by changing pad byte to 2 and brute-force changing
+            # second-to-last byte to 2 as well
+            tweaked = prior[-1] ^ (2^1)
+            tweaked = struct.pack("B", tweaked)
+            for j in range(1,256):
+                guess = prior[-2] ^ j
+                guess = struct.pack("B", guess)
+                if self._oracle(self._ciphertext+prior[:-2]+guess+tweaked+final, self._iv):
+                    # XXX: Save the decrypted byte for later
+                    ret_val = buffertools.pkcs7Pad(pad_length)
+
+        return ret_val
+
+
+    # XXX: This could be generalized as a byte probe utility for a variety of attacks
+    def _test_value_set(self, prefix, suffix, value_set):
+        for b in value_set:
+            if self._thread_result != None:
+                # Stop if another thread found the result
+                break
+            if self._oracle(prefix+struct.pack("B",b)+suffix, self._iv):
+                self._thread_result = b
+                break
+
+
+    def decrypt_next_byte(self, prior, block, known_bytes, cache=True):
+        """Decrypts one byte of ciphertext by modifying the prior
+        ciphertext block at the same relative offset.
+
+        Arguments:
+        prior -- Ciphertext block appearing prior to the current target 
+        block -- Currently targeted ciphertext block
+        known_bytes -- Bytes in this block already decrypted
+
+        """
+
+        if(len(block)!=self.block_size):
+            raise InvalidBlockError
+        numKnownBytes = len(known_bytes)
+        
+        if(numKnownBytes >= self.block_size):
+            return known_bytes
+        
+        prior_prefix = prior[0:self.block_size-numKnownBytes-1]
+        base = prior[self.block_size-numKnownBytes-1]
+        # Adjust known bytes to appear as a PKCS 7 pad
+        suffix = [0]*numKnownBytes
+        for i in range(0,numKnownBytes):
+            suffix[i] ^= prior[0-numKnownBytes+i]^known_bytes[i]^(numKnownBytes+1)
+        suffix = struct.pack("B"*len(suffix),*suffix)+block
+
+        # XXX: catch any signal exceptions, such as ^C, and communicate
+        #      this back to the rest of the script so it can end immediately 
+        for x in range(0, 1+self.retries):
+            # Each thread spawned searches a subset of the next byte's 
+            # 256 possible values
+            self._thread_result = None
+            threads = []
+            for i in range(0,self.threads):
+                t = threading.Thread(target=self._test_value_set, 
+                                     args=(self._ciphertext+prior_prefix, suffix, range(i,256,self.threads)))
+                t.start()
+                threads.append(t)
+                
+            for t in threads:
+                t.join()
+                
+            # If a byte fails to decrypt, it could be because the prior
+            # block's decrypted value violates UTF-8 decoding rules, or
+            # because it randomly introduced a delimiter that causes
+            # problems.  If retries are enabled, we insert an additional
+            # random block before the prior block so that the decrypted
+            # value can be changed.
+            if self._thread_result == None:
+                if x < self.retries:
+                    self.log_message("Value of a byte could not be determined. Retrying...")
+                    prior_prefix = bytes([random.getrandbits(8) for i in range(self.block_size)]) + prior_prefix
+            else:
+                break
+
+        if self._thread_result == None:
+            self.log_message("Value of a byte could not be determined.  Current plaintext suffix: "+ repr(self.decrypted))
+            raise Exception #XXX: custom exception
+        
+        decrypted = struct.pack("B",self._thread_result^base^(numKnownBytes+1))
+        if cache:
+            self.decrypted = decrypted + self.decrypted
+        #  Return previous bytes together with current byte
+        return decrypted+known_bytes 
+    
+
+    def decrypt_block(self, prior, block, last_bytes=b'', cache=True):
+        """Decrypts the block of ciphertext provided as a parameter.
+
+        """
+
+        while(len(last_bytes)!=self.block_size):
+            last_bytes = self.decrypt_next_byte(prior, block, last_bytes, cache)
+
+        self.log_message("Decrypted block: %s" % repr(last_bytes))
+        return last_bytes
+
+
+    def decrypt(self):
+        """Decrypts the previously supplied ciphertext. If the IV was
+        not provided, it assumes a IV of zero bytes.
+
+        """
+
+        if len(self.decrypted) == 0:
+            # First decrypt the padding (quick to decrypt and good sanity check)
+            pad_bytes = self.probe_padding()
+            if pad_bytes == None:
+                # XXX: custom exception
+                self.log_message("Could not determine pad length")
+                raise Exception
+            
+            self.decrypted = pad_bytes
+
+
+        # Start where we left off last, whether that be with just a pad,
+        # or with additional decrypted blocks.
+
+        # number of bytes in any partially decrypted blocks
+        num_partial = len(self.decrypted) % self.block_size
+
+        # number of blocks fully decrypted
+        finished_blocks = len(self.decrypted) // self.block_size
+
+        # contents of the partial block
+        partial = self.decrypted[0:num_partial]
+
+        # contents of fully decrypted blocks
+        decrypted = self.decrypted[num_partial:]
+        
+        blocks = buffertools.splitBuffer(self._ciphertext, self.block_size)
+
+        # Start with the partially decrypted block at the end, and work
+        # our way to the front.  Don't decrypt the very first block of
+        # the ciphertext yet.
+        for i in range(len(blocks)-1-finished_blocks, 0, -1):
+            decrypted = self.decrypt_block(blocks[i-1], blocks[i], partial) + decrypted
+            partial = b''
+                
+        # Finally decrypt first block
+        if finished_blocks < len(blocks):
+            decrypted = self.decrypt_block(self._iv, blocks[0], partial) + decrypted
+        
+        # Remove the padding and return
+        return buffertools.stripPKCS7Pad(decrypted, self.block_size, self.log_fh)
+
+
+    def encrypt_block(self, plaintext, ciphertext):
+        """Encrypts a block of plaintext.  This is accomplished by
+        decrypting the supplied ciphertext and then computing the prior
+        block needed to create the desired plaintext at the ciphertext's
+        location. 
+
+        Returns the calculated prior block and the provided ciphertext
+        block as a tuple.
+
+        """
+        if len(plaintext) != self.block_size or len(plaintext) != len(ciphertext):
+            raise InvalidBlockError(self.block_size,len(plaintext))
+
+        ptext = self.decrypt_block(b'\x00'*self.block_size, ciphertext, cache=False)
+        prior = buffertools.xorBuffers(ptext, plaintext)
+        self.log_message("Encrypted block: %s to %s with prior %s" % (repr(plaintext), repr(ciphertext), repr(prior)))
+        return prior,ciphertext
+    
+    
+    def encrypt(self,plaintext, ciphertext=None):
+        """Encrypts a plaintext value through "CBC-R" style prior-block
+        propagation.
+        
+        Returns a tuple of the IV and ciphertext.  
+
+        NOTE: If your target messages do not include an IV with the
+        ciphertext, you can instead opt to encrypt a suffix of the
+        message and include the IV in the the middle of the ciphertext as 
+        if it were an encrypted block. This one block alone will decrypt
+        to an uncontrollable random value, but with careful placement,
+        this might be ok.
+
+        """
+        
+        blocks = buffertools.splitBuffer(buffertools.pkcs7PadBuffer(plaintext, self.block_size), 
+                                         self.block_size)
+        if ciphertext != None:
+            if len(ciphertext) % self.block_size != 0:
+                raise InvalidBlockError(self.block_size,len(ciphertext))
+            num_cblocks = (len(ciphertext) // self.block_size) - 1
+            del blocks[0-num_cblocks:] # we've already encrypted these
+            prior = ciphertext[0:self.block_size]
+            
+        elif (len(self.decrypted) >= self.block_size
+            and len(self._ciphertext) >= 2*self.block_size):
+            # If possible, reuse work from prior decryption efforts on original
+            # message for last block
+            old_prior = self._ciphertext[0-self.block_size*2:0-self.block_size]
+            final_plaintext = self.decrypted[0-self.block_size:]
+            prior = buffertools.xorBuffers(old_prior,
+                                           buffertools.xorBuffers(final_plaintext, blocks[-1]))
+            ciphertext = self._ciphertext[0-self.block_size:]
+            del blocks[-1]
+        else:
+            # Otherwise, select a random last block and generate the prior block
+            prior = struct.pack("B"*self.block_size, 
+                                     *[random.getrandbits(8) for i in range(self.block_size)])
+            ciphertext = b''
+
+        try:
+            # Continue generating all prior blocks
+            for i in range(len(blocks)-1, -1, -1):
+                prior,cblock = self.encrypt_block(blocks[i],prior)
+                ciphertext = cblock+ciphertext
+        except Exception as e:
+            self.log_message("Encryption failure. prior+ciphertext: %s" % repr(prior+ciphertext))
+
+        # prior as IV
+        return prior,ciphertext
Index: releases/0.0.1/lib/bletchley/blobtools.py
===================================================================
--- releases/0.0.1/lib/bletchley/blobtools.py	(revision 61)
+++ releases/0.0.1/lib/bletchley/blobtools.py	(revision 61)
@@ -0,0 +1,529 @@
+'''
+A collection of tools to assist in analyzing encrypted blobs of data
+
+Copyright (C) 2011-2012 Virtual Security Research, LLC
+Author: Timothy D. Morgan, Jason A. Donenfeld
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import sys
+import base64
+import binascii
+import traceback
+import fractions
+import operator
+import functools
+import itertools
+from . import buffertools
+
+
+# urllib.parse's functions are not well suited for encoding/decoding
+# bytes or managing encoded case 
+def _percentEncode(binary, plus=False, upper=True):
+    fmt = "%%%.2X"
+    if upper:
+        fmt = "%%%.2x"
+
+    ret_val = bytearray(b'')
+    for c in binary:
+        if c not in b'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789':
+            ret_val.extend((fmt % c).encode('ascii'))
+        elif plus and (c == 20):
+            ret_val.extend(b'+')
+        else:
+            ret_val.append(c)
+    
+    return ret_val
+
+
+def _percentDecode(binary, plus=False):
+    if plus:
+        binary = binary.replace(b'+', b' ')
+    if binary == b'':
+        return b''
+    chunks = binary.split(b'%')
+
+    ret_val = chunks[0]
+    for chunk in chunks[1:]:
+        if len(chunk) < 2:
+            return None
+        try:
+            ret_val += bytes([int(chunk[0:2], 16)]) + chunk[2:]
+        except:
+            #traceback.print_exc()
+            #print(repr(chunk), repr(binary))
+            return None
+            
+    return ret_val
+
+
+# abstract class
+class DataEncoding(object):
+    charset = frozenset(b'')
+    extraneous_chars = b''
+    dialect = None
+    name = None
+    priority = None
+
+    def __init__(self, dialect=''):
+        self.dialect = dialect
+
+    def isExample(self, blob):
+        sblob = frozenset(blob)
+        if self.charset != None and not sblob.issubset(self.charset):
+            return False
+        return self.extraTests(blob)
+    
+    def extraTests(self, blob):
+        """May return True, False, or None, for is an example, isn't an
+        example, or unknown, respectively. 
+
+        """
+        return True
+
+    def decode(self, blob):
+        return None
+
+    def encode(self, blob):
+        return None
+
+
+class base64Encoding(DataEncoding):
+    name = 'base64'
+    def __init__(self, dialect='rfc3548'):
+        super(base64Encoding, self).__init__(dialect)
+        if dialect.startswith('rfc3548'):
+            self.c62 = b'+'
+            self.c63 = b'/'
+            self.pad = b'='
+        elif dialect.startswith('filename'):
+            self.c62 = b'+'
+            self.c63 = b'-'
+            self.pad = b'='
+        elif dialect.startswith('url1'):
+            self.c62 = b'-'
+            self.c63 = b'_'
+            self.pad = b'='
+        elif dialect.startswith('url2'):
+            self.c62 = b'-'
+            self.c63 = b'_'
+            self.pad = b'.'
+        elif dialect.startswith('url3'):
+            self.c62 = b'_'
+            self.c63 = b'-'
+            self.pad = b'.'
+        elif dialect.startswith('url4'):
+            self.c62 = b'-'
+            self.c63 = b'_'
+            self.pad = b'!'
+        elif dialect.startswith('url5'):
+            self.c62 = b'+'
+            self.c63 = b'/'
+            self.pad = b'$'
+        elif dialect.startswith('url6'):
+            self.c62 = b'*'
+            self.c63 = b'/'
+            self.pad = b'='
+        elif dialect.startswith('otkurl'):
+            self.c62 = b'-'
+            self.c63 = b'_'
+            self.pad = b'*'
+        elif dialect.startswith('xmlnmtoken'):
+            self.c62 = b'.'
+            self.c63 = b'-'
+            self.pad = b'='
+        elif dialect.startswith('xmlname'):
+            self.c62 = b'_'
+            self.c63 = b':'
+            self.pad = b'='
+        
+        if 'newline' in dialect:
+            self.extraneous_chars = b'\r\n'
+
+        self.charset = frozenset(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
+                                 +b'abcdefghijklmnopqrstuvwxyz0123456789'
+                                 +self.c62+self.c63+self.pad+self.extraneous_chars)
+
+    def _guessPadLength(self, nopad_len):
+        length = ((4 - nopad_len % 4) % 4)
+        if length != 3:
+            return length
+        return None
+
+    def extraTests(self, blob):
+        for c in self.extraneous_chars:
+            blob = blob.replace(bytes([c]), b'')
+
+        nopad = blob.rstrip(self.pad)
+        padlen_guess = self._guessPadLength(len(nopad))
+        if padlen_guess == None:
+            return False
+
+        # we don't accept bad pads, only missing pads
+        if self.dialect.endswith('nopad'):
+            return self.pad not in blob
+
+        # pad must not appear in the middle of the 
+        # string and must be the correct length at the end
+        return (self.pad not in nopad) and (len(blob) == len(nopad)+padlen_guess)
+
+    def decode(self, blob):
+        for c in self.extraneous_chars:
+            blob = blob.replace(bytes(c), b'')
+
+        if self.dialect.endswith('nopad'):
+            if self.pad in blob:
+                raise Exception("Unpadded base64 string contains pad character")
+
+            padlen = self._guessPadLength(len(blob))
+            if padlen == None:
+                raise Exception("Invalid length for unpadded base64 string.")
+
+            blob = blob+(self.pad*padlen)
+
+        if not self.dialect.startswith('rfc3548'):
+            table = bytes.maketrans(self.c62+self.c63+self.pad, b'+/=')
+            blob = blob.translate(table)
+
+        return base64.standard_b64decode(blob)
+
+
+    def encode(self, blob):
+        ret_val = base64.standard_b64encode(blob)
+
+        if not self.dialect.startswith('rfc3548'):
+            table = bytes.maketrans(b'+/=', self.c62+self.c63+self.pad)
+            ret_val = ret_val.translate(table)
+
+        if ret_val != None and self.dialect.endswith('nopad'):
+            ret_val = ret_val.rstrip(self.pad)
+
+        return ret_val
+
+
+class base32Encoding(DataEncoding):
+    name = 'base32'
+    def __init__(self, dialect='rfc3548upper'):
+        super(base32Encoding, self).__init__(dialect)
+        if dialect.startswith('rfc3548upper'):
+            self.pad = b'='
+            self.charset = frozenset(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'+self.pad)
+
+        elif dialect.startswith('rfc3548lower'):
+            self.pad = b'='
+            self.charset = frozenset(b'abcdefghijklmnopqrstuvwxyz234567'+self.pad)
+
+    def _guessPadLength(self, nopad_len):
+        pad_lengths = {0:0, 7:1, 5:3, 4:4, 2:6}
+        return pad_lengths.get(nopad_len%8, None)  
+
+    def extraTests(self, blob):
+        nopad = blob.rstrip(self.pad)
+        padlen_guess = self._guessPadLength(len(nopad))
+        if padlen_guess == None:
+            return False
+
+        # we don't accept bad pads, only missing pads
+        if self.dialect.endswith('nopad'):
+            return self.pad not in blob
+
+        # pad must not appear in the middle of the 
+        # string and must be the correct length at the end
+        return (self.pad not in nopad) and (len(blob) == len(nopad)+padlen_guess)
+
+
+    def decode(self, blob):
+        if self.dialect.endswith('nopad'):
+            if self.pad in blob:
+                raise Exception("Unpadded base32 string contains pad character")
+
+            padlen = self._guessPadLength(len(blob))
+            if padlen == None:
+                raise Exception("Invalid length for unpadded base64 string.")
+
+            blob = blob+(self.pad*padlen)
+
+        return base64.b32decode(blob.upper())
+
+
+    def encode(self, blob):
+        ret_val = base64.b32encode(blob)
+
+        if ret_val != None and self.dialect.endswith('nopad'):
+            ret_val = ret_val.rstrip(self.pad)
+
+        if 'lower' in self.dialect:
+            ret_val = ret_val.lower()
+        else:
+            ret_val = ret_val.upper()
+
+        return ret_val
+
+
+class hexEncoding(DataEncoding):
+    name = 'hex'
+    def __init__(self, dialect='mixed'):
+        super(hexEncoding, self).__init__(dialect)
+        if 'mixed' in dialect:
+            self.charset = frozenset(b'ABCDEFabcdef0123456789')
+        elif 'upper' in dialect:
+            self.charset = frozenset(b'ABCDEF0123456789')            
+        elif 'lower' in dialect:
+            self.charset = frozenset(b'abcdef0123456789')
+
+
+    def extraTests(self, blob):
+        return (len(blob) % 2 == 0)
+
+    def decode(self, blob):
+        return binascii.a2b_hex(blob)
+
+    def encode(self, blob):
+        if 'upper' in self.dialect:
+            return binascii.b2a_hex(blob).upper()
+        if 'lower' in self.dialect:
+            return binascii.b2a_hex(blob).lower()
+        else:
+            return binascii.b2a_hex(blob)
+
+
+class percentEncoding(DataEncoding):
+    name = 'percent'
+    def __init__(self, dialect='mixed'):
+        super(percentEncoding, self).__init__(dialect)
+        self.charset = None
+        if 'mixed' in dialect:
+            self.hexchars = frozenset(b'ABCDEFabcdef0123456789')
+        elif 'upper' in dialect:
+            self.hexchars = frozenset(b'ABCDEF0123456789')            
+        elif 'lower' in dialect:
+            self.hexchars = frozenset(b'abcdef0123456789')
+
+    def extraTests(self, blob):
+        chunks = blob.split(b'%')
+        if len(chunks) < 2:
+            return None
+        for c in chunks[1:]:
+            if len(c) < 2:
+                return False
+            if (c[0] not in self.hexchars) or (c[1] not in self.hexchars):
+                return False
+        return True
+
+    def decode(self, blob):
+        plus = False
+        if 'plus' in self.dialect:
+            plus = True
+        return _percentDecode(blob, plus=plus)
+
+    def encode(self, blob):
+        upper = True
+        plus = False
+        if 'plus' in self.dialect:
+            plus = True
+        if 'lower' in self.dialect:
+            upper = False
+
+        return _percentEncode(blob, plus=plus, upper=upper)
+
+
+priorities = [
+    (hexEncoding, 'upper', 100),
+    (hexEncoding, 'lower', 101),
+    (hexEncoding, 'mixed', 102),
+    (base32Encoding, 'rfc3548upper', 150),
+    (base32Encoding, 'rfc3548lower', 151),
+    (base32Encoding, 'rfc3548upper-nopad', 160),
+    (base32Encoding, 'rfc3548lower-nopad', 161),
+    (base64Encoding, 'rfc3548', 200),
+    (base64Encoding, 'rfc3548-nopad', 201),
+    (base64Encoding, 'rfc3548-newline', 202),
+    (base64Encoding, 'filename', 210),
+    (base64Encoding, 'filename-nopad', 211),
+    (base64Encoding, 'url1', 230),
+    (base64Encoding, 'url1-nopad', 231),
+    (base64Encoding, 'otkurl', 235),
+    (base64Encoding, 'otkurl-nopad', 236),
+    (base64Encoding, 'url2', 240),
+    (base64Encoding, 'url2-nopad', 241),
+    (base64Encoding, 'url3', 250),
+    (base64Encoding, 'url3-nopad', 251),
+    (base64Encoding, 'url4', 260),
+    (base64Encoding, 'url4-nopad', 261),
+    (base64Encoding, 'url5', 265),
+    (base64Encoding, 'url5-nopad', 266),
+    (base64Encoding, 'url6', 267),
+    (base64Encoding, 'url6-nopad', 268),
+    (base64Encoding, 'xmlnmtoken', 270),
+    (base64Encoding, 'xmlnmtoken-nopad', 271),
+    (base64Encoding, 'xmlname', 280),
+    (base64Encoding, 'xmlname-nopad', 281),
+    (percentEncoding, 'upper-plus', 400),
+    (percentEncoding, 'upper', 401),
+    (percentEncoding, 'lower-plus', 410),
+    (percentEncoding, 'lower', 411),
+    (percentEncoding, 'mixed-plus', 420),
+    (percentEncoding, 'mixed', 421),
+    ]
+
+encodings = {}
+for enc,d,p in priorities:
+    e = enc(d)
+    e.priority = p
+    encodings["%s/%s" % (enc.name, d)] = e
+
+def supportedEncodings():
+    e = list(encodings.keys())
+    e.sort()
+    return e
+
+
+def possibleEncodings(blob):
+    likely = set()
+    possible = set()
+    for name,encoding in encodings.items():
+        result = encoding.isExample(blob)
+        if result == True:
+            likely.add(name)
+        elif result == None:
+            possible.add(name)
+    return likely,possible
+
+
+def encodingIntersection(blobs):
+    ret_val = set(encodings.keys())
+    p = set(encodings.keys())
+    for b in blobs:
+        likely,possible = possibleEncodings(b)
+        ret_val &= likely | possible
+        p &= possible
+    return ret_val - p
+
+
+def bestEncoding(encs):
+    priority = 999999999
+    best = None
+    for e in encs:
+        if encodings[e].priority < priority:
+            best = e
+            priority = encodings[e].priority
+    return best
+
+
+def decode(encoding, blob):
+    return encodings[encoding].decode(blob)
+
+def encode(encoding, blob):
+    return encodings[encoding].encode(blob)
+
+def decodeAll(encoding, blobs):
+    return [encodings[encoding].decode(b) for b in blobs]
+
+def encodeAll(encoding, blobs):
+    return [encodings[encoding].encode(b) for b in blobs]
+
+def decodeChain(decoding_chain, blob):
+    for decoding in decoding_chain:
+        blob = decode(decoding, blob)
+    return blob
+
+def encodeChain(encoding_chain, blob):
+    for encoding in encoding_chain:
+        blob = encode(encoding, blob)
+    return blob
+
+def getLengths(s):
+    lengths = set()
+    for bin in s:
+        lengths.add(len(bin))
+    lengths = list(lengths)
+    lengths.sort()
+    return lengths
+
+
+def maxBlockSize(blob_lengths):
+    divisor = 0
+    for bl in blob_lengths:
+        divisor = fractions.gcd(divisor, bl)
+
+    return divisor
+
+
+allTrue = functools.partial(functools.reduce, (lambda x,y: x and y))
+
+def checkCommonBlocksizes(lengths):
+    common_block_sizes = (8,16,20)
+    ret_val = []
+    for cbs in common_block_sizes:
+        gcdIsCBS = (lambda x: fractions.gcd(x,cbs)==cbs)
+        if allTrue(map(gcdIsCBS, lengths)):
+            ret_val.append(cbs)
+    return ret_val
+
+
+def int2binary(x, bits=8):
+        """
+        Integer to binary
+        Count is number of bits
+        """
+        return "".join(map(lambda y:str((x>>y)&1), range(bits-1, -1, -1)))
+
+
+#XXX: move this to buffertools
+def smartPermutateBlobs(blobs, block_size=8):
+    """
+    Intelligently permutates through blocks in blobs.
+    If the same blob shows up in the same place for
+    every blob, the resultant permutations will have
+    this property as well.
+    blobs should be an array containing blobs
+    block_size should be an integer block_size or an
+    array of block sizes.
+    """
+
+    if len(blobs) == 0:
+        return
+
+    if not isinstance(block_size, (int, long)):
+        for size in block_size:
+             for blob in smartPermutateBlobs(blobs, size):
+                 yield blob
+        return
+
+    # First we find the indexes of the chunks that are different
+    different = set()
+    for combo in itertools.combinations(blobs, 2):
+        different |= set(buffertools.blockWiseDiff(block_size, combo[0], combo[1]))
+    
+    # Next we form a set containing the chunks that are different
+    different_chunks = []
+    for blob in blobs:
+        different_chunks.extend([blob[i * block_size:(i + 1) * block_size] for i in different])
+    # Remove duplicates
+    different_chunks = set(different_chunks)
+    
+    # We want to know which chunks are the same, too
+    chunk_len = len(blobs[0]) / block_size
+    same = set(range(0, chunk_len)) - different
+
+    # Now let's mix and match the differnet blocks, for all possible lengths
+    for i in range(1, chunk_len + 1):
+        for mix in itertools.permutations(different_chunks, i):
+            # We add back in the part that stays the same
+            for j in same:
+                mix.insert(j, blobs[0][j * block_size:(j + 1) * block_size])
+            mix = "".join(mix)
+            if mix in blobs:
+                continue
+            yield mix 
Index: releases/0.0.1/lib/bletchley/buffertools.py
===================================================================
--- releases/0.0.1/lib/bletchley/buffertools.py	(revision 61)
+++ releases/0.0.1/lib/bletchley/buffertools.py	(revision 61)
@@ -0,0 +1,146 @@
+'''
+A collection of tools to manipulate buffers of encrypted content
+
+Copyright (C) 2011-2012 Virtual Security Research, LLC
+Author: Timothy D. Morgan, Jason A. Donenfeld
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import sys
+import zlib
+
+# Computes the block-wise differences between two strings
+# Blobs must be the same length and their length must be a multiple of block_size
+#
+# Returns a list of block numbers that are different from one another.  
+# Examples:
+# blockWiseDiff(8, '1234567812345678', '12345678XXXXXXXX')
+# => [1]
+#
+# blockWiseDiff(1, '12345', '12345')
+# => []
+#
+
+def blockWiseDiff(block_size, blob1, blob2):
+    if len(blob1) != len(blob2):
+        sys.stderr.write("ERROR: Ciphertexts not the same length.\n")
+        return None
+
+    if (len(blob1) % block_size) != 0:
+        sys.stderr.write("ERROR: Ciphertexts do not have an even multiple of blocks.\n")
+        return None
+
+    blocks1 = [blob1[o:o+block_size] for o in range(0,len(blob1),block_size)]
+    blocks2 = [blob2[o:o+block_size] for o in range(0,len(blob2),block_size)]
+
+    ret_val = []
+    for b in range(0,len(blocks1)):
+        if blocks1[b] != blocks2[b]:
+            ret_val.append(b)
+
+    return ret_val
+
+
+
+def blockWiseColorMap(block_size, blobs):
+    '''
+    Accepts a sequence of blobs and compares all individual blocks 
+    (of block_size) within those blobs.  Returns a dictionary where the
+    keys are blocks from the original blobs whose values were repeated
+    at least once. (Unique blocks in the original blobs will not be
+    represented in the return value.) The values of the returned
+    dictionary are a 32bit integer hash value of the blocks they are
+    associated with.
+    '''
+    block_counts = {}
+    for blob in blobs:
+        for block_off in range(0,len(blob),block_size):
+            block = blob[block_off:block_off+block_size]
+            count = block_counts.get(block, None)
+            if count == 0:
+                block_counts[block] = 1 # more than one exists
+            elif count == None:
+                block_counts[block] = 0  # one exists
+
+    colors = {}
+    for block,count in block_counts.items():
+        if count == 1:
+            # mask needed for portability
+            colors[block] = zlib.crc32(block)%0xFFFFFFFF 
+
+    return colors
+
+
+
+# XORs two buffers (bytes/bytearrays) and returns result
+#
+# If buffers not the same length, returned buffer length 
+# will be that of the shorter buffer.
+#
+def xorBuffers(buff1, buff2):
+    max_len = min(len(buff1), len(buff2))
+
+    ret_val = bytearray(buff1[0:max_len])
+    other = bytearray(buff2[0:max_len])
+    for i in range(0,len(ret_val)):
+        ret_val[i] ^= other[i]
+
+    return ret_val
+
+
+def splitBuffer(buf, block_size):
+	'''
+	Splits a buffer into evenly sized blocks.
+	'''
+	return [buf[i:i + block_size] for i in range(0, len(buf), block_size)]
+
+def iterBuffer(buf, block_size):
+	'''
+	Iterates through a buffer in evenly sized blocks.
+	'''
+	return (buf[i:i + block_size] for i in range(0, len(buf), block_size))
+
+def pkcs7PadBuffer(buf, block_size):
+	'''
+	Pads the end of a buffer using PKCS#5/PKCS#7 padding.
+	'''
+	padding = block_size - (len(buf) % block_size)
+	return buf + pkcs7Pad(padding)
+
+def pkcs7Pad(length):
+	return bytes([length]*length)
+
+def stripPKCS7Pad(decrypted, block_size=16, log_file=None):
+    '''
+    Validates a plaintext containing a PKCS5/7 pad, then returns the plaintext 
+    without the pad.
+    '''
+    if len(decrypted) % block_size != 0:
+        if log_file:
+            log_file.write("Can't strip PKCS7 pad.  Plaintext not correct length\n")
+        return None
+
+    length = decrypted[-1]
+    if length > block_size:
+        if log_file:
+            log_file.write("Can't strip PKCS7 pad.  Final byte length (%d) too large.\n" % length)
+        return None
+
+    pad = decrypted[0-length:]
+    if pad != pkcs7Pad(length):
+        if log_file:
+            log_file.write("Can't strip PKCS7 pad.  Pads don't match. (%s != %s)\n" % (repr(pad), repr(pkcs7Pad(length))))
+        return None
+
+    return decrypted[0:0-length]
Index: releases/0.0.1/lib/bletchley/chosen.py
===================================================================
--- releases/0.0.1/lib/bletchley/chosen.py	(revision 61)
+++ releases/0.0.1/lib/bletchley/chosen.py	(revision 61)
@@ -0,0 +1,75 @@
+'''
+A collection of tools to assist in analyzing encrypted blobs of data 
+through chosen plaintext attacks.
+
+Copyright (C) 2011-2012 Virtual Security Research, LLC
+Author: Timothy D. Morgan
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import sys
+from buffertools import blockWiseDiff
+
+
+# Chosen plaintext attack on ECB encryption
+#
+# The encryptionOracle should accept a single string argument, 
+# encrypt that argument as part of a larger (unknown) plaintext string,
+# and then return the ciphertext.
+#
+# This function will then return a dictionary with information about the 
+# algorithm and chosen string attributes, including:
+#  block_size - the algorithm's block size
+#  chosen_offset - the chosen string's offset within the plaintext
+#  fragment_length - the length of a chosen from the chosen_offset to the
+#                    end of its current block
+#
+def ECB_FindChosenOffset(encryptionOracle):
+    ret_val = {}
+
+    # Guaranteed to have one block boundary on 128 bit block ciphers
+    chosen_length = 17
+    chosen = 'O'*chosen_length
+    base = encryptionOracle(chosen)
+
+    chosen = 'X' + 'O'*(chosen_length-1)
+    test_result = encryptionOracle(chosen)
+
+    different_blocks = blockWiseDiff(1, base, test_result)
+    block_size = len(different_blocks)
+    # Sanity check
+    different_blocks = blockWiseDiff(block_size, base, test_result)
+    if different_blocks == None:
+        sys.stderr.write("ERROR: Block size test yielded undiff-able ciphertexts.\n")
+        return None
+    if len(different_blocks) > 1:
+        sys.stderr.write("ERROR: Block size test yielded multiple altered blocks (not ECB mode?).\n")
+        return None
+
+    for i in range(2,chosen_length):
+        chosen = 'X'*i + 'O'*(chosen_length-i)
+        test_result = encryptionOracle(chosen)
+        different_blocks = blockWiseDiff(block_size, base, test_result)
+        
+        if different_blocks == None or len(different_blocks) == 0 or len(different_blocks) > 2:
+            sys.stderr.write("ERROR: Offset detection yielded inconsistent block diffs.\n")
+            return None
+        if len(different_blocks) == 2:
+            break
+
+    ret_val['block_size'] = block_size
+    ret_val['fragment_length'] = i-1
+    ret_val['chosen_offset'] = max(different_blocks)*block_size - ret_val['fragment_length']
+
+    return ret_val
Index: releases/0.0.1/lib/bletchley/chosenct.py
===================================================================
--- releases/0.0.1/lib/bletchley/chosenct.py	(revision 61)
+++ releases/0.0.1/lib/bletchley/chosenct.py	(revision 61)
@@ -0,0 +1,68 @@
+'''
+A collection of tools to assist in analyzing encrypted data 
+through chosen ciphertext attacks.
+
+Copyright (C) 2012-2013 Virtual Security Research, LLC
+Author: Timothy D. Morgan
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import sys
+import threading
+import struct
+import queue
+
+def probe_bytes(checker, ciphertext, values, max_threads=1):
+    '''For each offset in the ciphertext, XORs each of the values with
+    it and sends it to the checker to determine what kind of response or
+    error message was generated.
+
+    TODO
+    '''
+    if max_threads < 1:
+        return None
+
+    ciphertext = bytearray(ciphertext)
+    values = bytearray(values)
+
+    ret_val = {}
+    num_threads = min(len(values),max_threads)
+    threads = []
+    for j in range(0,len(ciphertext)):
+        prefix = ciphertext[0:j]
+        target = ciphertext[j]
+        suffix = ciphertext[j+1:]
+        results = queue.Queue()
+        for i in range(0,num_threads):
+            subset = [values[s] for s in range(i,len(values),num_threads)]
+            t = threading.Thread(target=probe_worker, 
+                                 args=(checker, prefix, suffix, target,
+                                       subset, results))
+            t.start()
+            threads.append(t)
+
+        for t in threads:
+            t.join()
+
+        ret_val[j] = {}
+        while not results.empty():
+            ret_val[j].update(results.get())
+
+    return ret_val
+
+
+def probe_worker(checker, prefix, suffix, target, value_subset, results):
+    for v in value_subset:
+        results.put({v:checker(prefix+bytearray((v^target,))+suffix)})
+
Index: releases/0.0.1/lib/bletchley/known.py
===================================================================
--- releases/0.0.1/lib/bletchley/known.py	(revision 61)
+++ releases/0.0.1/lib/bletchley/known.py	(revision 61)
@@ -0,0 +1,33 @@
+'''
+A collection of tools to assist in analyzing encrypted blobs of data 
+through known plaintext attacks.
+
+Copyright (C) 2011-2012 Virtual Security Research, LLC
+Author: Timothy D. Morgan
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+'''
+
+from buffertools import xorBuffers
+
+# Attack OFB mode with static IV
+#
+def OFB_TestKnownPlaintext(plaintext, ciphertext1, ciphertext2):
+    ret_val = []
+
+    p1p2 = xorBuffers(ciphertext1,ciphertext2)
+    for i in range(0,len(p1p2)-len(plaintext)):
+        ret_val.append(xorBuffers(p1p2[i:i+len(plaintext)], plaintext))
+        
+    return ret_val
+
Index: releases/0.0.1/src/nextrand.c
===================================================================
--- releases/0.0.1/src/nextrand.c	(revision 61)
+++ releases/0.0.1/src/nextrand.c	(revision 61)
@@ -0,0 +1,79 @@
+/*
+Simple tool to generate the next [num] subsequent random numbers using Java
+Random.nextInt(), given any two sequential outputs of this method.
+
+Copyright (C) 2012 Virtual Security Research, LLC
+Author: Dan J. Rosenberg
+Updates by: Timothy D. Morgan
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Lesser General Public License, version 3,
+ as published by the Free Software Foundation.
+
+ 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#define MULTIPLIER 25214903917L
+#define ADDEND 11L
+#define MASK ((1L << 48) - 1) 
+
+unsigned long long seed;
+
+int nextInt()
+{
+  seed = (seed * MULTIPLIER + ADDEND) & MASK;
+  return (int) (seed >> 16);
+}
+
+
+int main(int argc, char **argv)
+{
+  int i, num;
+  unsigned long long r1, r2;
+  
+  if (argc != 4) 
+  {
+    fprintf(stderr, "[-] Usage: %s rand1 rand2 num\n", argv[0]);
+    fprintf(stderr, 
+            "[-] Note that rand1 and rand2 must be signed integers returned in sequence\n"
+            "    from a single Java Random instance.  Values provided must be generated\n"
+            "    by Random.nextInt() which was called with no arguments.\n");
+    return 1;
+  }
+  
+  r1 = atoi(argv[1]);
+  r2 = atoi(argv[2]);
+  num = atoi(argv[3]);
+  
+  for (i = 0; i < 65536; i++) 
+  {
+    seed = (r1 << 16) + i;
+    if ((unsigned int)(((seed * MULTIPLIER + ADDEND) & MASK) >> 16) == (unsigned int)r2) 
+      break;
+    
+    seed = 0;
+  }
+  
+  if (!seed) {
+    fprintf(stderr, "[-] Seed not found.\n");
+    return 1;
+  }
+  
+  fprintf(stderr, "[+] Seed %.12llX found based on provided values: ", seed);
+  fprintf(stderr, "%d %d\n", (int)r1, nextInt());
+  fprintf(stderr, "[+] Next %d values:\n", num);
+  
+  for (i = 0; i < num; i++) 
+    printf("%d\n", nextInt());
+  
+  return 0;
+}
