131 lines
5.3 KiB
Python
131 lines
5.3 KiB
Python
from flask import Flask, render_template, request, make_response, redirect
|
||
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',)
|
||
|
||
|
||
@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_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 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
|
||
freecaptcha_cookie = str(uuid.uuid4())
|
||
resp.set_cookie('freecaptcha_cookie', freecaptcha_cookie)
|
||
captcha_solutions[freecaptcha_cookie] = answer
|
||
|
||
return resp
|