FreeCAPTCHA/app.py

86 lines
4.0 KiB
Python
Raw Normal View History

2022-04-11 23:44:17 -04:00
from flask import Flask, render_template, request, make_response, redirect
from freecaptcha import captcha
import uuid
2023-06-12 09:56:17 -04:00
import os
2023-06-29 17:01:21 -04:00
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
2022-04-11 23:44:17 -04:00
app = Flask(__name__, static_url_path='', static_folder='static',)
2023-06-29 17:01:21 -04:00
@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']
2023-06-30 18:40:02 -04:00
n = payload['n']
2023-06-29 17:01:21 -04:00
encrypted_correct_answer_bytes = base64.b64decode(b64_and_encrypted_correct_answer)
correct_answer = cipher.decrypt(encrypted_correct_answer_bytes).decode('utf-8').split('|||')[0]
2023-06-30 18:40:02 -04:00
## Redirect to the original page the user wanted - with a token letting that they can validate from us that says that the user passed a specific captcha attempt (we will sign the attempt with a code we give them with the captcha, like an id, so they know it was that specific attempt)
2023-06-29 17:01:21 -04:00
return f'''
The correct answer was {correct_answer}
2023-06-30 18:40:02 -04:00
You flipped it {int(captcha_attempt) % n}
2023-06-29 17:01:21 -04:00
'''
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()
2023-06-30 18:40:02 -04:00
n = 6
answer, options = captcha.captchafy(image_path, n)
2023-06-29 17:01:21 -04:00
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)
2023-06-12 09:56:17 -04:00
2023-06-29 17:01:21 -04:00
ciphertext = base64.b64encode(encrypted_bytes).decode('utf-8')
token = jwt.encode({
'encrypted_correct_answer': ciphertext,
2023-06-30 18:40:02 -04:00
'salt': str(salt),
'n': n
2023-06-29 17:01:21 -04:00
}, 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.
2023-06-30 18:40:02 -04:00
else:
return "Unsupported HTTP method."
2023-06-29 17:01:21 -04:00
# Flask should take care of unsupported methods for us.