diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2143ebc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# docker build -t death-code . +# docker run -d --restart unless-stopped --name death-code -p 2020:80 death-code +# curl 127.0.0.1:2020 + +FROM tiangolo/uwsgi-nginx-flask:python3.8 +LABEL maintainer="Andrew Dinh " +LABEL version="0.3.0" + +EXPOSE 80 + +ADD ./requirements.txt /app/requirements.txt +RUN python3 -m pip install --upgrade pip && python3 -m pip install -r /app/requirements.txt + +ADD ./templates /app/templates/ +ADD ./main.py /app/main.py + +HEALTHCHECK CMD curl http://localhost \ No newline at end of file diff --git a/LICENSE b/LICENSE index 0ad25db..29ebfa5 100644 --- a/LICENSE +++ b/LICENSE @@ -658,4 +658,4 @@ specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see -. +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf5f41d --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Death Code + +Death Code is an entirely self-hosted web application that utilizes [Sharmir's Secret Sharing](https://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing) to share secrets after you die. After splitting a secret among a group of people, the secret can only be reconstructed when a sufficient number of people combine their parts together, presumably only after you are gone from this earth. + +## Building + +1. Install `git`, `docker`, and `docker-compose` +2. + +```bash +git clone https://github.com/andrewkdinh/death-code.git +cd death-code +cp .env.example .env +# Edit .env +git clone https://github.com/daniel-e/rust-captcha.git rust-captcha +docker-compose up -d +``` + +## Credits + +- Built with Python Flask and Docker + +Mirrors: [GitHub](https://github.com/andrewkdinh/death-code) (main), [Gitea](https://gitea.andrewkdinh.com/andrewkdinh/death-code) + +Licensed under [AGPL](./LICENSE) | Copyright (c) 2021 Andrew Dinh diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e2990a3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +version: '3.0' +services: + death-code: + container_name: death-code + build: + context: . + restart: unless-stopped + ports: + - 33284:80 + depends_on: + - rust-captcha + environment: + - END_URL=${END_URL} # If attempt is successful, redirect to this URL + - DOMAIN=${DOMAIN} # The domain this will be hosted on + - COEFFICIENTS_AND_MOD=${COEFFICIENTS_AND_MOD} # For example, [3, 5, 23] represents the function f(x)=3x+5 mod 23 + - ALIVE_PATH=${ALIVE_PATH} # Path to deny allowing attempts immediately and start the countdown (optional) + - DAYS_TO_ALLOW=${DAYS_TO_ALLOW} # How many days to GET alive endpoint until death code starts allowing attemps (optional) + - DEAD_PATH=${DEAD_PATH} # Path to start allowing attemps immediately (optional) + rust-captcha: + container_name: rust-captcha + build: + context: ./rust-captcha/docker + restart: unless-stopped + diff --git a/main.py b/main.py new file mode 100755 index 0000000..6e7cc89 --- /dev/null +++ b/main.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 + +from flask import Flask, render_template, redirect, request +import requests +from typing import List +from datetime import datetime +import os +import json + +END_URL = os.getenv('END_URL') +COEFFICIENTS_AND_MOD = json.loads(os.getenv('COEFFICIENTS_AND_MOD')) # For example, [3, 5, 23] represents the function f(x)=3x+5 mod 23 +DOMAIN = os.getenv('DOMAIN') +DAYS_TO_ALLOW = os.getenv('DAYS_TO_ALLOW') # How many days to GET endpoint until death code functions +if DAYS_TO_ALLOW: + DAYS_TO_ALLOW = int(DAYS_TO_ALLOW) +# DAYS_TO_ALLOW = 7 +ALIVE_PATH = os.getenv('ALIVE_PATH') # Path to deny allowing attempts immediately and start the countdown (optional) +# ALIVE_PATH = "i-am-alive" +DEAD_PATH = os.getenv('DEAD_PATH') # Path to start allowing attemps immediately (optional) +# DEAD_PATH = "i-am-dead" + +LAST_ENDPOINT_GET: datetime = None + +app = Flask(__name__) + +@app.route("/", methods=["GET", "POST"]) +def index(): + global POLYNOMIAL, LAST_ENDPOINT_GET, DAYS_TO_ALLOW + if request.method == "POST": + if ALIVE_PATH and DAYS_TO_ALLOW and LAST_ENDPOINT_GET and (datetime.today() - LAST_ENDPOINT_GET).days < DAYS_TO_ALLOW: + return render_template("message.html", message="Andrew is still alive. Try again another time") + captcha_id = request.form.get('captcha-id') + captcha_solution = request.form.get('captcha-solution') + v = captcha_validate(captcha_id, captcha_solution) + if not v[0]: + return render_template('message.html', message = "Failed captcha", attempts_left = v[1]) + try: + coords = [] + for key in request.form: + if 'x' == key[0]: + coords.append(Coordinate(int(request.form[key]), int(request.form['y' + key[1]]))) + if POLYNOMIAL.valid_combination(coords): + return render_template("congrats.html", polynomial=POLYNOMIAL, domain=DOMAIN) + return render_template("message.html", message="Those points weren't valid", attempts_left = v[1]) + except Exception as e: + print(e) + return render_template("message.html", message="Invalid data") + captcha = captcha_get(ttl = 300) + return render_template("index.html", polynomial = POLYNOMIAL, captcha_id = captcha[0], captcha_png = captcha[1]) + +@app.route("/", methods=["GET", "POST"]) +def attempt(attempt_num): + global POLYNOMIAL, LAST_ENDPOINT_GET, DAYS_TO_ALLOW + try: + attempt_num = int(attempt_num) + if request.method == "POST": + captcha_id = request.form.get('captcha-id') + captcha_solution = request.form.get('captcha-solution') + v = captcha_validate(captcha_id, captcha_solution) + if v[0]: + if ALIVE_PATH and LAST_ENDPOINT_GET and (datetime.today() - LAST_ENDPOINT_GET).days < DAYS_TO_ALLOW: + return render_template("message.html", message="Andrew is still alive. Try again another time") + num = int(attempt_num) + if num == POLYNOMIAL.x_zero_point: + return redirect(END_URL, code=302) + return render_template('message.html', message="Incorrect guess for f(0)", attempts_left = v[1]) + return render_template('message.html', message="Failed captcha", attempts_left = v[1]) + captcha = captcha_get(ttl = 30, difficulty="hard") + return render_template('attempt.html', captcha_id = captcha[0], captcha_png = captcha[1]) + except ValueError as e: + print(e) + return render_template('message.html', message="URL path must be an integer") + except Exception as e: + print(e) + return render_template('message.html', message="Error ocurred") + +if ALIVE_PATH and DAYS_TO_ALLOW: + @app.route("/" + ALIVE_PATH, methods=["GET"]) + def am_alive(): + global LAST_ENDPOINT_GET + LAST_ENDPOINT_GET = datetime.today() + return "OK" + + if DEAD_PATH: + @app.route("/" + DEAD_PATH, methods=["GET"]) + def am_dead(): + global LAST_ENDPOINT_GET + LAST_ENDPOINT_GET = None + return "OK" + +def captcha_validate(captcha_id: str, captcha_solution: str) -> List: + """ Validates a captcha and returns [success, trials_left] """ + response = requests.post(f"http://rust-captcha:8000/solution/{captcha_id}/{captcha_solution}", headers={'X-Client-ID': 'Death Code'}).json() + if response["error_code"] != 0: + print(f"http://rust-captcha:8000/solution/{captcha_id}/{captcha_solution}") + raise Exception(response) + if response["result"]["solution"] == "accepted": + return [True, 0] + return [False, response["result"]["trials_left"]] + +def captcha_get(max_tries: int = 3, ttl: int = 120, difficulty: str = "medium") -> List[str]: + """ Creates a captcha and returns [id, base64 encoded png] """ + response = requests.post(f"http://rust-captcha:8000/new/{difficulty}/{max_tries}/{ttl}", headers={'X-Client-ID': 'Death Code'}).json() + if response["error_code"] != 0: + raise Exception(response) + return [response["result"]["id"], response["result"]["png"]] + +class Coordinate: + def __init__(self, x: int, y: int): + self.x = x + self.y = y + + def __repr__(self) -> str: + return "({}, {})".format(self.x, self.y) + + def equals(self, coord): + """ Returns whether this coordinate is equal to the other """ + return self.x == coord.x and self.y == coord.y + +class Polynomial: + def __init__(self, coefficients: List, modulo: int): + while (coefficients[0] == 0): + coefficients = coefficients[1:] + + self.degree = len(coefficients) - 1 + self.coefficients = coefficients + self.modulo = modulo + self.x_zero_point = coefficients[-1] % modulo + + def __repr__(self): + i = 0 + s = "y=" + exponent = self.degree + while exponent > 1: + if s != "y=": + s += "+" + s += str(self.coefficients[i]) + "x^" + str(exponent) + exponent -= 1 + i += 1 + if i != len(self.coefficients): + if s != "y=": + s += "+" + s += str(self.coefficients[i]) + "x" + i += 1 + if i != len(self.coefficients): + if s != "y=": + s += "+" + s += str(self.coefficients[-1]) + return s + " mod " + str(self.modulo) + + def valid_combination(self, coordinates: List[Coordinate]) -> bool: + """ Returns whether there are enough valid coordinates in `coordinates` to extract this polyomial """ + count = 0 + seen_coords = [] + for coord in coordinates: + if self.valid_coord(coord) and all([not coord.equals(coordinate) for coordinate in seen_coords]): + count += 1 + seen_coords.append(coord) + return count >= self.degree + 1 + + def valid_coord(self, coord: Coordinate) -> bool: + """ Returns whether `coord` is a valid coordinate for this polynomial """ + exponent = self.degree + coefficients = self.coefficients + value = 0 + i = 0 + while exponent >= 0: + value += coefficients[i] * pow(coord.x, exponent) + exponent -= 1 + i += 1 + return value % self.modulo == coord.y + +POLYNOMIAL = Polynomial(COEFFICIENTS_AND_MOD[:-1], COEFFICIENTS_AND_MOD[-1]) + +if __name__ == "__main__": + app.run(host="0.0.0.0", debug=True, port=8888) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..be8330c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask~=1.1.2 +requests~=2.25.1 \ No newline at end of file diff --git a/templates/attempt.html b/templates/attempt.html new file mode 100644 index 0000000..6331660 --- /dev/null +++ b/templates/attempt.html @@ -0,0 +1,23 @@ + + + + + + Death Code + + + + + + +

Do this captcha because I don't trust y'all (captcha only valid for 30 seconds)

+
+ + + + +
+ + \ No newline at end of file diff --git a/templates/congrats.html b/templates/congrats.html new file mode 100644 index 0000000..062aceb --- /dev/null +++ b/templates/congrats.html @@ -0,0 +1,30 @@ + + + + + + Death Code + + + + + + +

Congrats, you got it! The polynomial is {{ polynomial }} and the point at x=0 is {{ polynomial.x_zero_point }}.

+ +

Now, visit https://{{ domain }}/{{ polynomial.x_zero_point }}

+ +

(A pink background just for you Vicki!)

+ + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..2c9aaf9 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,62 @@ + + + + + + Death Code + + + + + + +

Death Code

+
+

The polynomial is degree {{ polynomial.degree }}, so you'll need at least {{ polynomial.degree + 1 }} points to find it

+ +
+ {% for i in range(polynomial.degree + 1) %} +
+ + +
+ {% endfor %} +
+ +
+ + + +
+

Once you get the value of f(0), go to https://rip.andrewkdinh.com/f(0)

+ +
+

Here's resources with more information (for now, just contact info for others with a coordinate). Password should've been given to you

+ +
    +
  • Nextcloud
  • +
  • Abhik Ahuja's Nextcloud
  • +
  • +
    + MEGA +

    Decryption key: echo "U2FsdGVkX1/fG6A2L6D50C2kCsrMw/sVdrjxJF7SfLy5z/92gR3So4LXBm8CYl3K"|openssl base64 -d|openssl enc -d -pbkdf2 -aes-256-cbc -k "PASSWORD_HERE"

    +
    +
  • +
+ +
+

Some other tools to do Lagrange/polynomial interpolation:

+ + + diff --git a/templates/message.html b/templates/message.html new file mode 100644 index 0000000..7abbf36 --- /dev/null +++ b/templates/message.html @@ -0,0 +1,24 @@ + + + + + + Death Code + + + + + + +

{{ message }}

+ {% if attempts_left %} +

You have {{ attempts_left }} attempt(s) left

+ + {% elif attempts_left == 0 %} +

Captcha is now invalid (reload page after you go back)

+ + {% endif %} + + \ No newline at end of file