Index: /trunk/lib/bletchley/blobtools.py
===================================================================
--- /trunk/lib/bletchley/blobtools.py	(revision 64)
+++ /trunk/lib/bletchley/blobtools.py	(revision 65)
@@ -25,5 +25,4 @@
 import operator
 import functools
-import itertools
 from . import buffertools
 
@@ -479,51 +478,2 @@
         """
         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: /trunk/lib/bletchley/buffertools.py
===================================================================
--- /trunk/lib/bletchley/buffertools.py	(revision 64)
+++ /trunk/lib/bletchley/buffertools.py	(revision 65)
@@ -20,4 +20,5 @@
 import sys
 import zlib
+import itertools
 
 # Computes the block-wise differences between two strings
@@ -145,2 +146,50 @@
 
     return decrypted[0:0-length]
+
+
+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):
+        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(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 = b"".join(mix)
+            if mix in blobs:
+                continue
+            yield mix 
Index: /trunk/lib/bletchley/chosenct.py
===================================================================
--- /trunk/lib/bletchley/chosenct.py	(revision 64)
+++ /trunk/lib/bletchley/chosenct.py	(revision 65)
@@ -23,4 +23,92 @@
 import struct
 import queue
+import hashlib
+
+# Wish Python had a better function for this that escaped more characters
+_html_escape_table = {
+    "&": "&amp;",
+    '"': "&quot;",
+    "'": "&apos;",
+    ">": "&gt;",
+    "<": "&lt;",
+    "\n": "&#x0a",
+    "\r": "&#x0d",
+    }
+
+def _html_escape(text):
+    return "".join(_html_escape_table.get(c,c) for c in text)
+
+
+class ProbeResults:
+    '''TODO
+    '''
+    _values = None
+    _raw_table = None #indexes are byte offset, then XORed value
+    _messages = None
+    _html_header = """<head>
+<script>
+function displayMessage(id) 
+{
+  alert(document.getElementById(id).value);
+}
+</script>
+<style>
+td
+{
+  border-style: solid;
+  border-width: medium;
+  border-color: #FFFFFF;
+  border-spacing: 0px;
+  min-width: 100px;
+  max-width: 100px;
+  word-wrap: break-word;
+}
+</style></head>"""
+
+    
+    def __init__(self, ct_length, values):
+        self._ct_length = ct_length
+        self._values = values
+        self._raw_table = {}
+        self._messages = {}
+        return
+
+    def _generate_colors(self, s):
+        base=bytes(hashlib.md5(s).digest()[0:6])
+        color1 = "#%.2X%.2X%.2X" % tuple(base[:3])
+        color2 = "#%.2X%.2X%.2X" % tuple(base[3:])
+
+        return color1,color2
+
+
+    def toHTML(self):
+        maxlen = 20
+        ret_val = self._html_header
+        ret_val += '<table><tr><td>&nbsp;&nbsp;&nbsp;OFFSET<br /><br />VALUE</td>'
+
+        for offset in self._raw_table.keys():
+            ret_val += '<td>%d<br /><br /></td>' % offset
+        ret_val += '</tr>'
+
+        for v in self._values:
+            ret_val += '<tr><td>0x%.2X</td>' % v
+            for offset in range(0,self._ct_length):
+                message = self._raw_table[offset][v]
+                bg,fg = self._generate_colors(message)
+                message = message.decode('utf-8')
+
+                truncated = message[0:maxlen]
+                if len(message) > maxlen:
+                    truncated += '...'
+                msg_id = 'cell_%.2X_%.2X' % (offset, v)
+                ret_val += ('''<td style="background-color:%s; border-color:%s" onclick="displayMessage('%s')">'''
+                            '''<input type="hidden" id="%s" value="%s" />%s</td>\n''')\
+                            % (bg,fg, msg_id, msg_id, _html_escape(message), _html_escape(truncated))
+            ret_val += '</tr>'
+            
+        ret_val += '</table>'
+
+        return ret_val
+
 
 def probe_bytes(checker, ciphertext, values, max_threads=1):
@@ -29,5 +117,37 @@
     error message was generated.
 
-    TODO
+    Arguments:
+    checker -- A function which sends a specified ciphertext to the targeted
+               application and returns a string describing the kind of response
+               that was encountered.  This function should be thread-safe when
+               max_threads > 1.
+
+               This function should implement the prototype:
+                 def myChecker(ciphertext): ...
+
+               The function should return strings that are relevant to
+               the kind of overall response generated by the targeted
+               system or application.  For instance, if detailed error
+               messages are returned, then the important parts of those
+               errors should be returned.  If error messages are not
+               returned in some cases, then simple tokens that describe
+               the behavior of the response should suffice.  For
+               instance, if in some cases the application returns a
+               generic HTTP 500 error, in other cases it drops the TCP
+               connection, and still in other cases it doesn't return an
+               error, then the checker function could return "500",
+               "dropped", and "success" respectively for those cases.
+
+    ciphertext -- A ciphertext buffer (bytes/bytearray) that will be repeatedly
+               modified and tested using the checker function.
+
+    values --  A sequence of integers in the range [0..255].  These values
+               will be XORed with each byte in the ciphertext and tested, one
+               after another.  To make a single change to each byte in the 
+               ciphertext, provide something like [1].  To flip every bit 
+               in the entire ciphertext individually, supply: [1,2,4,8,16,32,64,128]
+
+    max_threads -- The maximum number of threads to run in parallel while 
+               testing modified ciphertexts.
     '''
     if max_threads < 1:
@@ -37,5 +157,10 @@
     values = bytearray(values)
 
-    ret_val = {}
+    # XXX: Improve threading model
+    #      Instead of forking threads and joining them for each byte, 
+    #      Generate all ciphertext variants up front, putting them in
+    #      a jobs queue, and then have persistent threads pull from 
+    #      the jobs queue  (or use a generator, rather than a queue)
+    ret_val = ProbeResults(len(ciphertext), values)
     num_threads = min(len(values),max_threads)
     threads = []
@@ -56,7 +181,9 @@
             t.join()
 
-        ret_val[j] = {}
+        # XXX: add functions to ProbeResults class to add results here, 
+        #      rather than accessing members directly.
+        ret_val._raw_table[j] = {}
         while not results.empty():
-            ret_val[j].update(results.get())
+            ret_val._raw_table[j].update(results.get())
 
     return ret_val
