initial commit
This commit is contained in:
28
pyproject.toml
Normal file
28
pyproject.toml
Normal 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
17
requirements.txt
Normal 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
0
sspoc/__init__.py
Normal file
156
sspoc/server.py
Normal file
156
sspoc/server.py
Normal 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
15
sspoc/static/index.html
Normal 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
109
sspoc/static/js/sspoc.js
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user