initial commit

This commit is contained in:
Walter Oggioni
2024-02-14 08:38:15 +08:00
parent 911419f30a
commit 336e4e8360
6 changed files with 325 additions and 0 deletions

28
pyproject.toml Normal file
View File

@@ -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"

17
requirements.txt Normal file
View File

@@ -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

0
sspoc/__init__.py Normal file
View File

156
sspoc/server.py Normal file
View File

@@ -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/<path:path>')
def send_javascript(path):
print(path)
return send_from_directory('static/js', path)
@app.route('/css/<path:path>')
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()

15
sspoc/static/index.html Normal file
View File

@@ -0,0 +1,15 @@
<!DOCTYPE HTML>
<html>
<head>
<script src="/js/sspoc.js" defer></script>
</head>
<body>
<form id="login-form">
<label for="username">Username</label>
<input id="username" type="text">
<label for="password">Password</label>
<input id="password" type="password">
<button id="login-button" type="button">Login</button>
</form>
</body>
</html>

109
sspoc/static/js/sspoc.js Normal file
View File

@@ -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);
});
});