From 336e4e83606d368b79b964d18275acef862d2200 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Wed, 14 Feb 2024 08:38:15 +0800 Subject: [PATCH] initial commit --- pyproject.toml | 28 +++++++ requirements.txt | 17 +++++ sspoc/__init__.py | 0 sspoc/server.py | 156 +++++++++++++++++++++++++++++++++++++++ sspoc/static/index.html | 15 ++++ sspoc/static/js/sspoc.js | 109 +++++++++++++++++++++++++++ 6 files changed, 325 insertions(+) create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 sspoc/__init__.py create mode 100644 sspoc/server.py create mode 100644 sspoc/static/index.html create mode 100644 sspoc/static/js/sspoc.js diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..5d6e8ad --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[build-system] +requires = ["setuptools>=65.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "sspoc" +version = "0.0.1" +authors = [ + { name="Walter Oggioni", email="walter.oggioni@accenture.com" }, +] +description = "Session security POC" +readme = "README.md" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] +dependencies = [ + "flask >= 3.0", + 'cryptography>=42.0.0', +] + +[tool.setuptools.package-data] +sspoc = ['static/**/*'] + +[project.scripts] +session-security-poc-server = "sspoc.server:main" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..68bb719 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +argcomplete==3.2.2 +blinker==1.7.0 +build==1.0.3 +cffi==1.16.0 +click==8.1.7 +cryptography==42.0.2 +Flask==3.0.2 +itsdangerous==2.1.2 +Jinja2==3.1.3 +MarkupSafe==2.1.5 +packaging==23.2 +pipx==1.4.3 +platformdirs==4.2.0 +pycparser==2.21 +pyproject_hooks==1.0.0 +userpath==1.9.1 +Werkzeug==3.0.1 diff --git a/sspoc/__init__.py b/sspoc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sspoc/server.py b/sspoc/server.py new file mode 100644 index 0000000..381f096 --- /dev/null +++ b/sspoc/server.py @@ -0,0 +1,156 @@ +import flask +from flask import Flask, send_from_directory +from flask import request +from functools import wraps +import base64 +import random +import json +from dataclasses import dataclass +from typing import Dict, Optional +from time import time +from hashlib import sha256 +from binascii import Error +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.asymmetric import padding +from cryptography.hazmat.primitives import hashes + +@dataclass +class SessionData: + nonce: bytes + user: str + +app = Flask(__name__) + +users = { + "user": "password", +} + +sessions: Dict[bytes, SessionData] = dict() + +# Authentication decorator +def token_required(f): + @wraps(f) + def decorator(*args, **kwargs): + cookie = request.cookies.get('Bearer') + if not cookie: + response = flask.Response() + response.status = 401 + response.data = "A valid cookie is missing!" + return response + binary_cookie: bytes = None + try: + binary_cookie = base64.b64decode(cookie) + except Error: + response = flask.Response() + response.status = 401 + response.data = "Cookie is invalid" + return response + session: Optional[SessionData] = sessions.get(binary_cookie) + if not session: + response = flask.Response() + response.status = 401 + response.data = "Cookie is invalid" + return response + + header_token = request.headers.get('x-token') + if not header_token: + response = flask.Response() + response.status = 401 + response.data = "Token is missing" + return response + binary_token: bytes = None + try: + binary_token = base64.b64decode(header_token) + except Error: + response = flask.Response() + response.status = 401 + response.data = "Token is invalid" + return response + current_tick: int = int(time()) // 10 + valid_tokens = [ + sha256(session.nonce + current_tick.to_bytes(8)).digest(), + sha256(session.nonce + (current_tick - 1).to_bytes(8)).digest() + ] + if binary_token not in valid_tokens: + response = flask.Response() + response.status = 401 + response.data = "Token is invalid" + return response + + return f(session.user, *args, **kwargs) + return decorator + +@app.route('/api/login', methods=['POST']) +def login(): + response = flask.Response() + if request.headers.get('Content-Type') != 'application/json': + response.status = 415 + return response + payload = json.loads(request.data) + user = payload.get('username') + if not user: + response.status = 401 + return response + password = users.get(user) + if not password or password != payload.get('password'): + response.status = 401 + return response + sr = random.SystemRandom() + nonce = sr.randbytes(16) + public_key_header = request.headers.get('public-key', None) + if not public_key_header: + response.status = 400 + return response + pem_key = f'-----BEGIN PUBLIC KEY-----\n{public_key_header}\n-----END PUBLIC KEY-----\n' + public_key = serialization.load_pem_public_key( + pem_key.encode(), + backend=default_backend() + ) + + response = flask.Response() + response.status = 200 + ciphertext = public_key.encrypt( + nonce, + padding.OAEP( + mgf=padding.MGF1(algorithm=hashes.SHA256()), + algorithm=hashes.SHA256(), + label=None + ) + ) + response.headers['nonce'] = base64.b64encode(ciphertext).decode() + cookie_bytes = sr.randbytes(16) + sessions[cookie_bytes] = SessionData(user=user, nonce=nonce) + cookie: str = base64.b64encode(cookie_bytes).decode() + response.set_cookie('Bearer', cookie, secure=True, httponly=True, samesite='Lax') + return response + + +@app.route('/') +def index(): + return send_from_directory('static', 'index.html') + + +@app.route('/js/') +def send_javascript(path): + print(path) + return send_from_directory('static/js', path) + + +@app.route('/css/') +def send_css(path): + return send_from_directory('static/css', path) + + +@app.route('/api/hello') +@token_required +def send_hello(user): + return f'hello {user}' + + +def main(): + app.run(host='0.0.0.0', port=1443, ssl_context='adhoc') + + +if __name__ == '__main__': + main() diff --git a/sspoc/static/index.html b/sspoc/static/index.html new file mode 100644 index 0000000..9cc2a12 --- /dev/null +++ b/sspoc/static/index.html @@ -0,0 +1,15 @@ + + + + + + +
+ + + + + +
+ + \ No newline at end of file diff --git a/sspoc/static/js/sspoc.js b/sspoc/static/js/sspoc.js new file mode 100644 index 0000000..306a0da --- /dev/null +++ b/sspoc/static/js/sspoc.js @@ -0,0 +1,109 @@ +function integerToBytes(integer, numBytes) { + const bytesArray = new Array(numBytes).fill(0); + for (let i = 0; i < numBytes; i++) { + bytesArray[numBytes - i - 1] = integer & 0xff; + integer >>= 8; + } + return Uint8Array.from(bytesArray); +} + +function concatenateUInt8Arrays(array1, array2) { + const newArray = new Uint8Array(array1.length + array2.length); + newArray.set(array1); + newArray.set(array2, array1.length); + return newArray; +} + +/* +Convert an ArrayBuffer into a string +from https://developer.chrome.com/blog/how-to-convert-arraybuffer-to-and-from-string/ +*/ +function ab2str(buf) { + return String.fromCharCode.apply(null, new Uint8Array(buf)); +} + +/* +Export the given key and write it into the "exported-key" space. +*/ +async function exportCryptoKeyPem(key) { + const exported = await window.crypto.subtle.exportKey("spki", key); + const exportedAsString = ab2str(exported); + const exportedAsBase64 = btoa(exportedAsString); + return exportedAsBase64; +} + +async function exportCryptoKey(key) { + const exported = await window.crypto.subtle.exportKey("jwk", key); + return JSON.stringify(exported, null, " "); +} + +async function createKeyPair() { + const crypto = window.crypto.subtle + const keyPair = await window.crypto.subtle.generateKey( + { + name: "RSA-OAEP", + modulusLength: 2048, + publicExponent: new Uint8Array([4, 0, 1]), + hash: "SHA-256", + }, + true, + ["encrypt", "decrypt"] + ); + return keyPair; +} + +let keyPair = createKeyPair(); +let loginForm = document.getElementById('login-form'); +let loginButton = document.getElementById('login-button'); +let nonce = null +loginButton.addEventListener('click', async evt => { + const publicKey = (await keyPair).publicKey; + const publicKeyPem = await exportCryptoKeyPem(publicKey); + fetch('api/login', { + method: 'POST', + headers: { + 'public-key' : publicKeyPem, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username: loginForm.username.value, password: loginForm.password.value}) + }).then(async response => { + const nonceHeader = response.headers.get('nonce'); + const encryptedNonce = atob(nonceHeader); + const privateKey = (await keyPair).privateKey; + const crypto = window.crypto.subtle; + const encryptedBuffer = Uint8Array.from(atob(nonceHeader), c => c.charCodeAt(0)); + nonce = await crypto.decrypt({ name: "RSA-OAEP" }, privateKey, encryptedBuffer) + .then(it => new Uint8Array(it)); + return response.text(); + }).then(text => { + let paragraph = document.createElement('p'); + paragraph.textContent = text; + document.body.appendChild(paragraph); + }); +}); + +let button = document.createElement('button'); +button.textContent = 'Press me' +document.body.appendChild(button); + +button.addEventListener('click', async evt => { + let header = {}; + if(nonce != null) { + const crypto = window.crypto.subtle; + const epochTick = Math.floor(new Date().getTime() / 10000) + const data = concatenateUInt8Arrays(nonce, integerToBytes(epochTick, 8)) + const hash = new Uint8Array(await crypto.digest("SHA-256", data)); + const token = btoa(Array.from(hash, byte => String.fromCharCode(byte)).join('')); + headers = { + 'x-token': token + }; + } + fetch('api/hello', { + method: 'GET', + headers + }).then(response => response.text()).then(text => { + let paragraph = document.createElement('p'); + paragraph.textContent = text; + document.body.appendChild(paragraph); + }); +});