diff --git a/__pycache__/app.cpython-39.pyc b/__pycache__/app.cpython-39.pyc index 9c89fc1..cc12dc6 100644 Binary files a/__pycache__/app.cpython-39.pyc and b/__pycache__/app.cpython-39.pyc differ diff --git a/app.py b/app.py index 88137de..b0e1cfe 100644 --- a/app.py +++ b/app.py @@ -1,52 +1,128 @@ from flask import Flask, render_template, request, make_response, redirect -from numpy import real from freecaptcha import captcha import uuid import os +from cryptography.fernet import Fernet +import jwt +import base64 + +# Generate a new AES key +key = Fernet.generate_key() + +JWT_SECRET_KEY = 'obviously this needs to be an env variable secret thingie.' + +# Create an instance of the Fernet cipher with the key +cipher = Fernet(key) + +# Encrypt a message +message = b'This is a secret message.' +ciphertext = cipher.encrypt(message) + +# Decrypt the ciphertext + + app = Flask(__name__, static_url_path='', static_folder='static',) -captcha_solutions = {} -captcha_solved = [] - -@app.route("/captcha2", methods=['GET']) -def captcha2(): - with open('templates/captcha2play.html') as play_template_file: - return play_template_file.read() - -@app.route("/captcha3", methods=['GET']) -def captcha3(): - return captcha.generate_captcha_html(os.listdir('static/images/')) @app.route("/captcha", methods=['GET', 'POST']) +def captcha_handler(): + if request.method == "POST": + captcha_attempt = len(list(request.form)) + token = request.cookies.get('Authorization').split('Bearer ')[-1] + try: + # TODO: set JWT to expire very soon. + payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=['HS256']) + b64_and_encrypted_correct_answer = payload['encrypted_correct_answer'] + encrypted_correct_answer_bytes = base64.b64decode(b64_and_encrypted_correct_answer) + correct_answer = cipher.decrypt(encrypted_correct_answer_bytes).decode('utf-8').split('|||')[0] + return f''' + The correct answer was {correct_answer} + You flipped it {captcha_attempt} + ''' + + except jwt.ExpiredSignatureError: + return 'Token expired. Please log in again.' + + except jwt.InvalidTokenError: + return 'Invalid token. Authentication required.' + ## Get their guess from POST body + ## Get JWT cookie + ## Decrypt payload + ## Check if guess (from post body) == correct answer (from decrypted JWT payload) + ## If so: give them a new JWT for winning the CAPTCHA, and forward them to the URL they originally wanted to access precaptcha (just like oauth does) + ## If not: Redirect them to the GET version of this same URL, with warning enabled to tell them they failed + if request.method == "GET": + image_path = captcha.random_image() + answer, options = captcha.captchafy(image_path) + print('the correct answer is: ', answer) + # remember to store the salt since we'll need it when we compare the hashes + salt = uuid.uuid4() + plaintext_bytes = bytes(str(answer) + '|||' + str(salt), 'utf-8') + encrypted_bytes = cipher.encrypt(plaintext_bytes) + + ciphertext = base64.b64encode(encrypted_bytes).decode('utf-8') + token = jwt.encode({ + 'encrypted_correct_answer': ciphertext, + 'salt': str(salt) + }, JWT_SECRET_KEY, algorithm='HS256') + + # Set the Authorization header cookie with the JWT + resp = make_response(captcha.generate_captcha_html(list(options))) + resp.set_cookie('Authorization', f'Bearer {token}') + + return resp + # Set JWT with encrypted answer as HTTP cookie + # issue: they could compare our encrypted answer with how we encrypt all numbers 1-6, since they will have seem them before. + # We could solve this with a salt, but then we have to store salts. We could include the salt in plaintext, but then they could build a rainbow table of our guess + salts (since there are only 6 possible guesses) + # the solution is to use salts big enough that we don't have to fear them repeating / being turned into a rainbow table. + # We will use UUID's as the salts. + # + # Anyway, we pass the data to our Jinja template and render it. + # Flask should take care of unsupported methods for us. + + + + ## Handle cookie + + ## Get random image + ## Generate + return captcha.generate_captcha_html(os.listdir('static/images/')) + +@app.route("/captcha_old", methods=['GET', 'POST']) def login(): # This means they just submitted a CAPTCHA # We need to see if they got it right incorrect_captcha = False if request.method == 'POST': - captcha_quess = request.form.get('captcha', None) + captcha_guess = len(list(request.form)) + print(request.form.get('captcha')) + # What if they POST with the cookie below absent? Uh oh... captcha_cookie = request.cookies.get('freecaptcha_cookie') + real_answer = captcha_solutions.get(captcha_cookie, None) if real_answer is not None: - if int(captcha_quess) == int(real_answer): + if captcha_guess == int(real_answer): captcha_solved.append(captcha_cookie) return redirect("/", code=302) else: incorrect_captcha = True + # Select an image image_path = captcha.random_image() # Generate list of rotated versions of image # and save which one is correct + # change answer to be the number of turns needed? answer, options = captcha.captchafy(image_path) + print(answer) # Provide the CAPTCHA options to the web page using the CAPTCHA resp = make_response(render_template("index.html", captcha_options=options, incorrect_captcha=incorrect_captcha)) - + resp = make_response(captcha.generate_captcha_html(list(options))) # Track this user with a cookie and store the correct answer - # by linking the cookie with the answer, we can check their answer - # later + # by linking the cookie with the answer, we can check their answer later freecaptcha_cookie = str(uuid.uuid4()) resp.set_cookie('freecaptcha_cookie', freecaptcha_cookie) captcha_solutions[freecaptcha_cookie] = answer diff --git a/freecaptcha/__pycache__/captcha.cpython-39.pyc b/freecaptcha/__pycache__/captcha.cpython-39.pyc index 6edb412..01780b4 100644 Binary files a/freecaptcha/__pycache__/captcha.cpython-39.pyc and b/freecaptcha/__pycache__/captcha.cpython-39.pyc differ diff --git a/freecaptcha/captcha.py b/freecaptcha/captcha.py index d0c25a5..a038d1f 100644 --- a/freecaptcha/captcha.py +++ b/freecaptcha/captcha.py @@ -3,6 +3,7 @@ import os from PIL import Image import base64 from io import BytesIO +from collections import deque import os @@ -13,7 +14,7 @@ def generate_layers(images, index=0): return f''' ''' @@ -102,22 +103,14 @@ def rotate_img(img_path, angle): def captchafy(img_path, n=6): angles = calculate_angles(n) - rotated_imgs = [ - { - 'original': False, - 'image': rotate_img(img_path, angle) - } for angle in angles] - - rotated_imgs[0]['original'] = True + rotated_imgs = deque( + [rotate_img(img_path, angle) for angle in angles][::-1] * 3 + ) + random_shift = random.randint(1, n) - random.shuffle(rotated_imgs) + rotated_imgs.rotate(-random_shift) + return random_shift % n, rotated_imgs - correct_img = None - for index, img in enumerate(rotated_imgs): - if img['original']: - correct_img = index - - return correct_img, [img['image'] for img in rotated_imgs] def random_image(dir='images/'): dir_contents = os.listdir(dir)