Index: /bin/bletchley-decode
===================================================================
--- /bin/bletchley-decode	(revision 20)
+++ /bin/bletchley-decode	(revision 20)
@@ -0,0 +1,44 @@
+#!/usr/bin/env python
+
+# Requires Python 2.7+
+
+'''
+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.')
+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.')
+options = parser.parse_args()
+
+input_file = sys.stdin
+if options.input_file is not None:
+    input_file = file(options.input_file, 'rb')
+
+blob = input_file.read()
+
+specified_encodings = options.encoding_chain.split(',')
+sys.stdout.write(blobtools.decodeChain(specified_encodings, blob))
Index: /bin/bletchley-encode
===================================================================
--- /bin/bletchley-encode	(revision 20)
+++ /bin/bletchley-encode	(revision 20)
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+
+# Requires Python 2.7+
+
+'''
+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.')
+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.')
+options = parser.parse_args()
+
+input_file = sys.stdin
+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()
+sys.stdout.write(blobtools.encodeChain(specified_encodings, blob))
Index: /doc/TODO
===================================================================
--- /doc/TODO	(revision 19)
+++ /doc/TODO	(revision 20)
@@ -3,7 +3,5 @@
 
 * PaddingOracle
-  - change print statements to configurable debug statements
-
-* New tools for easy scripted encoding/decoding at command line
+  - Enable resumption of decryption in case of failure
 
 
Index: /lib/bletchley/CBC/__init__.py
===================================================================
--- /lib/bletchley/CBC/__init__.py	(revision 19)
+++ /lib/bletchley/CBC/__init__.py	(revision 20)
@@ -35,7 +35,8 @@
       WTLS" by Serge Vaudenay (2002)
 
-    POA objects are not thread-safe.  If multiple threads need to work
+    POA objects are not caller thread-safe.  If multiple threads need to work
     simultaneously on the same ciphertext and oracle, create a
-    separate instance.
+    separate instance. POA objects can execute tasks internally using
+    multiple threads, however.
 
     """
@@ -63,5 +64,5 @@
          results in a correct padding upon decryption and False
          otherwise.  This function should implement the prototype:
-           def myOracle(ciphertext, iv)
+           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
@@ -210,4 +211,5 @@
         
         if self._thread_result == None:
+            self.log_message("Value of a byte could not be determined.  Current plaintext suffix: "+ repr(self.decrypted))
             raise Exception
 
@@ -230,6 +232,6 @@
     # XXX: Add logic to begin where decryption previously left off
     def decrypt(self):
-        """Decrypts a message using CBC mode. If the IV is not provided,
-        it assumes a null IV.
+        """Decrypts the previously supplied ciphertext. If the IV was
+        not provided, it assumes a IV of zero bytes.
 
         """
@@ -237,29 +239,55 @@
         blocks = buffertools.splitBuffer(self._ciphertext, self.block_size)
 
-        final = blocks[-1]
-        iv = self._iv
-        if iv == None:
-            iv = '\x00'*self.block_size
-        if len(blocks) == 1:
-            # If only one block present, then try to use IV as prior
-            prior = iv
-        else:
-            prior = blocks[-2]
-
-        # Decrypt last block, starting with padding (quicker to decrypt)
-        pad_bytes = self.probe_padding(prior, final)
-        decrypted = self.decrypt_block(prior, final, pad_bytes)
-
-        # Now decrypt all other blocks except first block
-        for i in range(len(blocks)-2, 0, -1):
-            decrypted = self.decrypt_block(blocks[i-1], blocks[i]) + decrypted
-
-        # Finally decrypt first block
-        decrypted = self.decrypt_block(iv, blocks[0]) + decrypted
-        
+        if len(self.decrypted) == 0:
+            
+            final = blocks[-1]
+            iv = self._iv
+            if iv == None:
+                iv = '\x00'*self.block_size
+            if len(blocks) == 1:
+                # If only one block present, then try to use IV as prior
+                prior = iv
+            else:
+                prior = blocks[-2]
+
+            # Decrypt last block, starting with padding (quicker to decrypt)
+            pad_bytes = self.probe_padding(prior, final)
+            decrypted = self.decrypt_block(prior, final, pad_bytes)
+
+            # Now decrypt all other blocks except first block
+            for i in range(len(blocks)-2, 0, -1):
+                decrypted = self.decrypt_block(blocks[i-1], blocks[i]) + decrypted
+
+            # Finally decrypt first block
+            decrypted = self.decrypt_block(iv, blocks[0]) + decrypted
+        
+        # Start where we left off last
+        # XXX: test this
+        else: 
+            num_partial = len(self.decrypted) % self.block_size
+            finished_blocks = len(self.decrypted) / self.block_size
+            partial = self.decrypted[0:num_partial]
+            decrypted = self.decrypted[num_partial:]
+
+            for i in range(-1-finished_blocks, 0, -1):
+                decrypted = self.decrypt_block(blocks[i-1], blocks[i], partial)
+                partial = ''
+
+            # Finally decrypt first block
+            decrypted = self.decrypt_block(iv, blocks[0]) + decrypted
+            
         return buffertools.stripPKCS7Pad(decrypted)
 
 
     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))
@@ -270,7 +298,18 @@
     
     
-    # XXX: Add option to encrypt only the last N blocks.  Supplying a shorter
-    #      plaintext and subsequent concatenation can easily achieve this as well...
     def encrypt(self,plaintext):
+        """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 as if it were a ciphertext block.
+        This block 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)
Index: /lib/bletchley/blobtools.py
===================================================================
--- /lib/bletchley/blobtools.py	(revision 19)
+++ /lib/bletchley/blobtools.py	(revision 20)
@@ -41,7 +41,13 @@
     def isExample(self, blob):
         sblob = frozenset(blob)
-        return ((self.charset == None or sblob.issubset(self.charset)) and self.extraTests(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
 
@@ -254,5 +260,5 @@
         chunks = blob.split('%')
         if len(chunks) < 2:
-            return False
+            return None
         for c in chunks[1:]:
             if len(c) < 2:
@@ -320,17 +326,23 @@
 
 def possibleEncodings(blob):
-    ret_val = set()
+    likely = set()
+    possible = set()
     for name,encoding in encodings.items():
-        if encoding.isExample(blob):
-            ret_val.add(name)
-    return ret_val
+        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:
-        ret_val &= possibleEncodings(b)
-
-    return ret_val
+        likely,possible = possibleEncodings(b)
+        ret_val &= likely | possible
+        p &= possible
+    return ret_val - p
 
 
@@ -404,4 +416,5 @@
 
 
+#XXX: move this to buffertools
 def smartPermutateBlobs(blobs, block_size=8):
     """
