initial commit with WiP code
This commit is contained in:
commit
c57f762789
|
@ -0,0 +1,2 @@
|
|||
__pycache__
|
||||
openpgp.min.js
|
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Register</title>
|
||||
<meta charset="utf-8">
|
||||
<script src="openpgp.min.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="registered.js"></script>
|
||||
<script>
|
||||
"use strict"
|
||||
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Data collection settings</h1>
|
||||
<form>
|
||||
<h2>Form 1</h2>
|
||||
<p>
|
||||
<label>Encrypt for the following keys:</label><br>
|
||||
<select id="encryptionKeys1" multiple></select>
|
||||
</p>
|
||||
<h2>Form 2</h2>
|
||||
<p>
|
||||
<label>Encrypt for the following keys:</label><br>
|
||||
<select id="encryptionKeys2" multiple></select>
|
||||
</p>
|
||||
<button>Submit</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,4 @@
|
|||
"use strict"
|
||||
|
||||
const SRV = "127.0.0.1:8000"
|
||||
const FETCH_PROTOCOL = "http"
|
|
@ -0,0 +1,62 @@
|
|||
import sqlite3
|
||||
|
||||
class DataBase():
|
||||
def __init__(self, dbfile):
|
||||
self._connection = sqlite3.connect(dbfile)
|
||||
self._init_database()
|
||||
|
||||
def _init_database(self):
|
||||
cursor = self._connection.cursor()
|
||||
cursor.execute("PRAGMA foreign_keys = ON")
|
||||
cursor.execute("""CREATE TABLE IF NOT EXISTS config
|
||||
(key TEXT NOT NULL PRIMARY KEY,
|
||||
value TEXT)""")
|
||||
cursor.execute("""CREATE TABLE IF NOT EXISTS users
|
||||
(fingerprint TEXT NOT NULL PRIMARY KEY,
|
||||
access_level INTEGER NOT NULL)""")
|
||||
cursor.execute("""CREATE TABLE IF NOT EXISTS data_key_relation
|
||||
(data INTEGER NOT NULL,
|
||||
user TEXT NOT NULL,
|
||||
FOREIGN KEY(data) REFERENCES encrypted_data(id),
|
||||
FOREIGN KEY(user) REFERENCES users(fingerprint))""")
|
||||
cursor.execute("""CREATE TABLE IF NOT EXISTS encrypted_data
|
||||
(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
secret TEXT)""")
|
||||
self._connection.commit()
|
||||
|
||||
def addUser(self, fingerprint: str, access_level: int):
|
||||
cursor = self._connection.cursor()
|
||||
cursor.execute("INSERT INTO users VALUES (:fpr, :level)", { "fpr": fingerprint, "level": access_level })
|
||||
self._connection.commit()
|
||||
|
||||
def deleteUser(self, fingerprint):
|
||||
cursor = self._connection.cursor()
|
||||
cursor.execute("DELETE FROM users WHERE fingerprint=:fpr", { "fpr": fingerprint })
|
||||
self._connection.commit()
|
||||
|
||||
def getConfig(self, key: str):
|
||||
cursor = self._connection.cursor()
|
||||
cursor.execute("SELECT value FROM config WHERE key=:key", { "key": key })
|
||||
value = cursor.fetchone()
|
||||
if value:
|
||||
value = value[0]
|
||||
return value
|
||||
|
||||
def getUser(self, fingerprint=None):
|
||||
cursor = self._connection.cursor()
|
||||
if fingerprint == None:
|
||||
cursor.execute("SELECT * FROM users")
|
||||
query_results = cursor.fetchall()
|
||||
if query_results is not None:
|
||||
return [{ "fingerprint": row[0], "access_level": row[1] } for row in query_results]
|
||||
else:
|
||||
cursor.execute("SELECT * FROM users wHERE fingerprint=:fpr", { "fpr": fingerprint })
|
||||
query_result = cursor.fetchone()
|
||||
if query_result is not None:
|
||||
return { "fingerprint": query_result[0], "access_level": query_result[1] }
|
||||
|
||||
def setConfig(self, key: str, value: str):
|
||||
cursor = self._connection.cursor()
|
||||
cursor.execute("""INSERT INTO config VALUES (:key, :value)
|
||||
ON CONFLICT(key) DO UPDATE SET value = :value""", { "key": key, "value": value })
|
||||
self._connection.commit()
|
|
@ -0,0 +1,121 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Register</title>
|
||||
<meta charset="utf-8">
|
||||
<script src="openpgp.min.js"></script>
|
||||
<script src="config.js"></script>
|
||||
<script src="registered.js"></script>
|
||||
<script>
|
||||
"use strict"
|
||||
|
||||
async function doDeleteKeys() {
|
||||
await deleteKeys(
|
||||
document.getElementById("passwordDel").value,
|
||||
() => { showKeysInitForm() },
|
||||
(error_message) => {
|
||||
document.getElementById("deleteError").innerHTML = "Error " + error_message
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function doInitKeys() {
|
||||
const pass1 = document.getElementById("password").value
|
||||
const pass2 = document.getElementById("passConfirmation").value
|
||||
if (pass1 !== pass2) {
|
||||
document.getElementById("passError").innerHTML = "Passwords doesn't match"
|
||||
setTimeout(() => document.getElementById("passError").innerHTML = "", 2500)
|
||||
return
|
||||
}
|
||||
|
||||
const myKeys = await initKeys(pass1)
|
||||
showKeyId(myKeys.id)
|
||||
document.getElementById("divInitKeys").style.display = "none"
|
||||
document.getElementById("divSubmitPubKey").style.display = "block"
|
||||
}
|
||||
|
||||
async function doSubmitPubKey() {
|
||||
await submitPubKey()
|
||||
if (isMyKeyOnserver(loadMyKeys().id)) {
|
||||
showKeysDeletionForm()
|
||||
}
|
||||
}
|
||||
|
||||
function showKeyId(id) {
|
||||
for (const e of document.getElementsByClassName("keyId")) {
|
||||
e.innerHTML = id
|
||||
}
|
||||
}
|
||||
|
||||
function showKeysDeletionForm() {
|
||||
for (const e of document.getElementsByClassName("keysInit")) {
|
||||
e.style.display = "none"
|
||||
}
|
||||
document.getElementById("divKeysDeletionForm").style.display = "block"
|
||||
}
|
||||
|
||||
function showKeysInitForm() {
|
||||
document.getElementById("divKeysDeletionForm").style.display = "none"
|
||||
document.getElementById("divInitKeys").style.display = "block"
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
const keys = loadMyKeys()
|
||||
if (keys !== null) {
|
||||
showKeyId(keys.id)
|
||||
const registered = await isMyKeyOnserver(keys.id)
|
||||
if (registered) {
|
||||
showKeysDeletionForm()
|
||||
} else {
|
||||
document.getElementById("divInitKeys").style.display = "none"
|
||||
document.getElementById("divSubmitPubKey").style.display = "block"
|
||||
}
|
||||
}
|
||||
}, false)
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="divInitKeys" class="keysInit">
|
||||
<form action="javascript:doInitKeys()">
|
||||
<h1>Create keys</h1>
|
||||
<p>
|
||||
<label>Password:</label><br>
|
||||
<input type="password" id="password">
|
||||
</p>
|
||||
<p>
|
||||
<label>Confirm password:</label><br>
|
||||
<input type="password" id="passConfirmation">
|
||||
</p>
|
||||
<p>(If you lose it, nobody can recover it.)</p>
|
||||
<p id="passError"></p>
|
||||
<button>Create</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="divSubmitPubKey" class="keysInit" style="display: none">
|
||||
<form action="javascript:doSubmitPubKey()">
|
||||
<h1>Your key</h1>
|
||||
<dl>
|
||||
<dt>Key fingerprint:</dt>
|
||||
<dd class="keyId"></dd>
|
||||
</dl>
|
||||
<button>Register public key</button>
|
||||
</form>
|
||||
</div>
|
||||
<div id="divKeysDeletionForm" style="display: none">
|
||||
<form action="javascript:doDeleteKeys()">
|
||||
<h1>Delete your keys and server access?</h1>
|
||||
<dl>
|
||||
<dt>Key fingerprint:</dt>
|
||||
<dd class="keyId"></dd>
|
||||
</dl>
|
||||
<p>
|
||||
<label>Password:</label><br>
|
||||
<input type="password" id="passwordDel">
|
||||
</p>
|
||||
<button>Delete</button>
|
||||
</form>
|
||||
<p id="deleteError"></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,138 @@
|
|||
"use strict"
|
||||
|
||||
const re_nonce = /^[A-Za-z0-9\-_=]+$/
|
||||
|
||||
async function deleteKeys(password, onDelete, onError) {
|
||||
await websocketApi(password, "key/delete",
|
||||
() => {},
|
||||
() => {
|
||||
localStorage.removeItem("my_keys")
|
||||
onDelete()
|
||||
},
|
||||
onError
|
||||
)
|
||||
}
|
||||
|
||||
async function getAllUserKeys(password, onAnswer, onError) {
|
||||
await websocketApi(password, "key/all", onAnswer, onError)
|
||||
}
|
||||
|
||||
async function initKeys(passphrase) {
|
||||
const kp = await openpgp.generateKey({
|
||||
userIds: [{}],
|
||||
curve: "ed25519",
|
||||
passphrase,
|
||||
})
|
||||
|
||||
const myKeys = {
|
||||
priv: kp.privateKeyArmored,
|
||||
pub: kp.publicKeyArmored,
|
||||
id: kp.key.getFingerprint(),
|
||||
}
|
||||
|
||||
localStorage.setItem("my_keys", JSON.stringify(myKeys))
|
||||
|
||||
return myKeys
|
||||
}
|
||||
|
||||
async function isMyKeyOnserver() {
|
||||
const res = await fetch(FETCH_PROTOCOL + "://" + SRV + "/key/" + loadMyKeys().id)
|
||||
if (res.ok) {
|
||||
const key = await res.text()
|
||||
if (key === "Unknown") {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
throw Error("Error while checking for key on server.")
|
||||
}
|
||||
}
|
||||
|
||||
function loadMyKeys() {
|
||||
return JSON.parse(localStorage.getItem("my_keys"))
|
||||
}
|
||||
|
||||
async function loadSrvKey() {
|
||||
//let key = localStorage.getItem("srv_key")
|
||||
let key = null
|
||||
if (key === null) {
|
||||
const res = await fetch(FETCH_PROTOCOL + "://" + SRV + "/key/srv")
|
||||
if (res.ok) {
|
||||
key = await res.text()
|
||||
//localStorage.setItem("srv_key", key)
|
||||
} else {
|
||||
throw Error("Error while fetching server key.")
|
||||
}
|
||||
}
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
async function submitPubKey() {
|
||||
const body = JSON.stringify({
|
||||
pubKey: loadMyKeys().pub
|
||||
})
|
||||
const res = await fetch(FETCH_PROTOCOL + "://" + SRV + "/key/add", {
|
||||
method: "POST",
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
async function websocketApi(password, request, onAnswer, onClose, onError) {
|
||||
const srvKey = await loadSrvKey()
|
||||
const { keys: [myKey] } = await openpgp.key.readArmored(loadMyKeys().priv)
|
||||
if (password)
|
||||
await myKey.decrypt(password)
|
||||
const message = JSON.stringify({ request })
|
||||
const { data: encrypted_request } = await openpgp.encrypt({
|
||||
message: openpgp.message.fromText(message),
|
||||
publicKeys: (await openpgp.key.readArmored(srvKey)).keys,
|
||||
privateKeys: [myKey]
|
||||
})
|
||||
const ws = new WebSocket("ws://" + SRV, "pgp-json")
|
||||
ws.onmessage = async message => {
|
||||
const decrypted = await openpgp.decrypt({
|
||||
message: await openpgp.message.readArmored(message.data),
|
||||
publicKeys: (await openpgp.key.readArmored(srvKey)).keys,
|
||||
privateKeys: [myKey]
|
||||
})
|
||||
if (decrypted.signatures[0].valid) {
|
||||
const message = JSON.parse(decrypted.data)
|
||||
if (message.request === request) {
|
||||
if (typeof message.nonce === "string" && re_nonce.test(message.nonce)) {
|
||||
const { data: signature_response } = await openpgp.encrypt({
|
||||
message: await openpgp.message.fromText(decrypted.data),
|
||||
publicKeys: (await openpgp.key.readArmored(srvKey)).keys,
|
||||
privateKeys: [myKey]
|
||||
})
|
||||
ws.send(signature_response)
|
||||
} else if (typeof message.payload === "object") {
|
||||
onAnswer(message.payload)
|
||||
ws.close(1000)
|
||||
} else {
|
||||
ws.close(1002, "Unsupported message type")
|
||||
onError("Received uknown message type: " + JSON.stringify(message))
|
||||
}
|
||||
} else {
|
||||
ws.close(1008, "Wrong request.")
|
||||
onError("Received packet for request " + message.request + ". " +
|
||||
"But we are asking for " + request + ".")
|
||||
}
|
||||
} else {
|
||||
ws.close(1008, "Invalid signature.")
|
||||
onError("Invalid signature from server.")
|
||||
}
|
||||
}
|
||||
ws.onclose = close => {
|
||||
if (close.code !== 1000) {
|
||||
onError("connection closed with code: " + close.code +
|
||||
", reason: " + close.reason)
|
||||
} else {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
ws.onopen = event => {
|
||||
ws.send(encrypted_request)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,301 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import gpg
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from gpg.gpgme import GPG_ERR_NO_ERROR, GPGME_DELETE_FORCE, gpgme_op_delete_ext
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
from secrets import token_urlsafe
|
||||
from sys import argv, exit as sysexit
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import websocket
|
||||
from database import DataBase
|
||||
|
||||
NONCE_BYTES = 128
|
||||
ADMIN_ACCESS = 100
|
||||
|
||||
class JsonMissingFieldException(Exception):
|
||||
def __init__(self, missing):
|
||||
self.missing = missing
|
||||
|
||||
|
||||
class RequestHandler(BaseHTTPRequestHandler):
|
||||
_RE_FILES = re.compile(r"^(/|/admin\.html|/config\.js|/registered\.js|/openpgp\.min\.js)$")
|
||||
_RE_KEY_FINGERPRINT = re.compile(r"^/key/([0-9a-fA-F]{40})$")
|
||||
_RE_PGP_JSON_REQUEST_FIELD = re.compile(r"^[0-9a-zA-Z_/]{3,100}$")
|
||||
_VALID_CONTENT_SUBTYPES = {"html", "javascript", "json", "plain"}
|
||||
|
||||
def _api_get_user_key(self, fingerprints=None):
|
||||
keys = list()
|
||||
if fingerprints == None:
|
||||
for user in self.server.db.getUser():
|
||||
keys.append({ "fingerprint": user["fingerprint"],
|
||||
"armored_key": (self.gpg_context.key_export(user["fingerprint"])).decode()})
|
||||
else:
|
||||
for fpr in fingerprints:
|
||||
if self.server.db.getUser(fpr) is not None:
|
||||
keys.append({ "fingerprint": fpr,
|
||||
"armored_key": (self.gpg_context.key_export(fpr)).decode()})
|
||||
else:
|
||||
websocket.close(self.wfile, 1008, "Asking for an unknown key.")
|
||||
self.websocket_connected = False
|
||||
return
|
||||
request_answer = websocket.encrypt_pgp_json({
|
||||
"request": self.websocket_pending_request,
|
||||
"payload": keys
|
||||
}, [self.websocket_client_key], self.gpg_context)
|
||||
websocket.send_message(self.wfile, request_answer)
|
||||
websocket.close(self.wfile)
|
||||
self.websocket_connected = False
|
||||
|
||||
def _api_register_user(self):
|
||||
try:
|
||||
req = self._read_json(["pubKey"])
|
||||
except JsonMissingFieldException:
|
||||
self._api_register_user_answer(400, f"Missing {missingField.missing}.")
|
||||
return
|
||||
results = self.server.gpg_context.key_import(req["pubKey"].encode())
|
||||
if results == "IMPORT_PROBLEM" or not results.considered:
|
||||
self._api_register_user_answer(400, "Invalid pubKey.")
|
||||
elif results.imported:
|
||||
access_level = ADMIN_ACCESS if len(self.server.db.getUser()) == 0 else 0 # first user get admin access level
|
||||
self.server.db.addUser(results.imports[0].fpr, access_level)
|
||||
print(f"Imported key {results.imports[0].fpr}.")
|
||||
self._api_register_user_answer(201, "Key registered.")
|
||||
elif results.unchanged:
|
||||
self._api_register_user_answer(200, "Key already on server.")
|
||||
else:
|
||||
self._api_register_user_answer(418, "What happened?")
|
||||
print(f"Tried to add following pubKey and got strange results:\n{req[pubKey]}\n\n{results}")
|
||||
|
||||
def _api_register_user_answer(self, code: int, message: str):
|
||||
self.send_response(code)
|
||||
self._set_content_type("json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({ "message": message }).encode())
|
||||
|
||||
def _api_unregister_user(self):
|
||||
result = gpgme_op_delete_ext(self.server.gpg_context.wrapped, self.websocket_client_key, GPGME_DELETE_FORCE)
|
||||
if result == GPG_ERR_NO_ERROR:
|
||||
self.server.db.deleteUser(self.websocket_client_key.fpr)
|
||||
print(f"Key {self.websocket_client_key.fpr} deleted.")
|
||||
websocket.close(self.wfile)
|
||||
else:
|
||||
print(f"Failed to delete key {self.websocket_client_key.fpr}, status code: {result}.")
|
||||
websocket.close(self.wfile, 1011)
|
||||
self.websocket_connected = False
|
||||
|
||||
def _handle_api_request(self, message: websocket.WebSocketMessage):
|
||||
signatures_keys = [self.websocket_client_key] if self.websocket_client_key is not None else None
|
||||
json_, signatures = websocket.decrypt_pgp_json(self.wfile, self.gpg_context,
|
||||
message, signatures_keys)
|
||||
|
||||
if len(signatures) != 1:
|
||||
websocket.close(self.wfile, 1002, "Multi-signatures message not implemented.")
|
||||
self.websocket_connected = False
|
||||
elif "request" not in json_ or not re.match(self._RE_PGP_JSON_REQUEST_FIELD, json_["request"]):
|
||||
websocket.close(self.wfile, 1002, "Missing or invalid request field.")
|
||||
self.websocket_connected = False
|
||||
|
||||
elif self.websocket_nonce is None:
|
||||
self.websocket_pending_request = json_["request"]
|
||||
self.websocket_client_key = self.gpg_context.get_key(signatures[0].fpr)
|
||||
self._request_signature()
|
||||
elif "nonce" not in json_ or json_["nonce"] != self.websocket_nonce:
|
||||
websocket.close(self.wfile, 1002, "Authentication failed.")
|
||||
self.websocket_connected = False
|
||||
|
||||
# API "routes"
|
||||
elif self.websocket_pending_request == "key/delete":
|
||||
if self.server.db.getUser(self.websocket_client_key.fpr)["access_level"] == ADMIN_ACCESS:
|
||||
websocket.close(self.wfile, 1008, "Cannot delete admin account.")
|
||||
self.websocket_connected = False
|
||||
return
|
||||
self._api_unregister_user()
|
||||
elif self.websocket_pending_request == "key/all":
|
||||
if self.server.db.getUser(self.websocket_client_key.fpr)["access_level"] == ADMIN_ACCESS:
|
||||
self._api_get_user_key()
|
||||
else:
|
||||
websocket.close(self.wfile, 1008, "API function reserved to admins.")
|
||||
self.websocket_connected = False
|
||||
else:
|
||||
websocket.close(self.wfile, 1002, "API function undefined.")
|
||||
self.websocket_connected = False
|
||||
|
||||
def _handle_websocket_request(self):
|
||||
self.websocket_client_key = None
|
||||
self.websocket_nonce = None
|
||||
self.websocket_pending_request = None
|
||||
self.gpg_context = gpg.Context(armor=True,
|
||||
home_dir=self.server.gpg_context.home_dir,
|
||||
offline=True,
|
||||
signers=[self.server.key])
|
||||
|
||||
try:
|
||||
proto = websocket.handshake(self, ["pgp-json"])
|
||||
if proto is None:
|
||||
websocket.close(self.wfile, 1002, "I only speak pgp-json")
|
||||
return
|
||||
self.websocket_connected = True
|
||||
print(f"WebSocket connection with {self.client_address[0]}:{self.client_address[1]}")
|
||||
except websocket.HandshakeError as error:
|
||||
print(f"WebSocket handshake failed. {error.get_reason(error.why)}: {error.what}={error.value}")
|
||||
return
|
||||
|
||||
try:
|
||||
while self.websocket_connected:
|
||||
message = websocket.read_next_message(self.rfile, self.wfile)
|
||||
self._handle_api_request(message)
|
||||
except websocket.WebSocketCloseException as close:
|
||||
print(f"WebSocket closed with code {close.code}, reason {close.reason}")
|
||||
|
||||
def _read(self):
|
||||
if not self.headers["Content-Length"]:
|
||||
return ""
|
||||
length = int(self.headers["Content-Length"])
|
||||
return self.rfile.read(length)
|
||||
|
||||
# implement type check for fields
|
||||
def _read_json(self, required_fields=list()):
|
||||
try:
|
||||
data = json.loads(self._read())
|
||||
except json.decoder.JSONDecodeError:
|
||||
data = {}
|
||||
for f in required_fields:
|
||||
if not f in data:
|
||||
raise JsonMissingFieldException(missing=f)
|
||||
return data
|
||||
|
||||
def _request_signature(self):
|
||||
self.websocket_nonce = token_urlsafe(NONCE_BYTES)
|
||||
signature_request = websocket.encrypt_pgp_json({
|
||||
"nonce": self.websocket_nonce,
|
||||
"request": self.websocket_pending_request
|
||||
}, [self.websocket_client_key], self.gpg_context)
|
||||
websocket.send_message(self.wfile, signature_request)
|
||||
|
||||
def _serve_err404(self):
|
||||
self.send_response(404)
|
||||
self._set_content_type("plain")
|
||||
self.end_headers()
|
||||
self.wfile.write("Not found".encode())
|
||||
|
||||
def _serve_file(self, filepath):
|
||||
if filepath == "/":
|
||||
filepath = "register.html"
|
||||
else:
|
||||
filepath = filepath[1:]
|
||||
self.send_response(200)
|
||||
if filepath.endswith("html"):
|
||||
self._set_content_type("html")
|
||||
else:
|
||||
self._set_content_type("javascript")
|
||||
self.end_headers()
|
||||
# assume files are utf-8 encoded
|
||||
with open(filepath, "rb") as f:
|
||||
output = f.read()
|
||||
self.wfile.write(output)
|
||||
|
||||
def _set_content_type(self, subtype):
|
||||
if subtype not in self._VALID_CONTENT_SUBTYPES:
|
||||
raise ValueError(f"RequestHandler._set_content_type: _type must be one of {self._VALID_CONTENT_SUBTYPES}.")
|
||||
_type = "text"
|
||||
if subtype == "json":
|
||||
_type = "application"
|
||||
self.send_header("Content-Type", f"{_type}/{subtype}; charset=utf-8")
|
||||
|
||||
def do_GET(self):
|
||||
filepath = re.match(self._RE_FILES, self.path)
|
||||
key_fingerprint = re.match(self._RE_KEY_FINGERPRINT, self.path)
|
||||
if self.headers.get("Upgrade") == "websocket":
|
||||
self._handle_websocket_request()
|
||||
elif key_fingerprint:
|
||||
key = self.server.gpg_context.key_export(key_fingerprint.group(1))
|
||||
key = key if key is not None else b"Unknown"
|
||||
self.send_response(200)
|
||||
self._set_content_type("plain")
|
||||
self.end_headers()
|
||||
self.wfile.write(key)
|
||||
elif self.path == "/key/srv":
|
||||
# May raise GPGMEerror
|
||||
key = self.server.gpg_context.key_export(self.server.db.getConfig("server_key"))
|
||||
self.send_response(200)
|
||||
self._set_content_type("plain")
|
||||
self.end_headers()
|
||||
self.wfile.write(key)
|
||||
elif filepath:
|
||||
self._serve_file(filepath.group(1))
|
||||
else:
|
||||
self._serve_err404()
|
||||
|
||||
def do_POST(self):
|
||||
if self.path == "/key/add":
|
||||
self._api_register_user()
|
||||
|
||||
class Server(HTTPServer):
|
||||
def __init__(self, listen_to, gpg_home, db):
|
||||
self.gpg_context = gpg.Context(armor=True, home_dir=gpg_home, offline=True)
|
||||
self.db = DataBase(db)
|
||||
self.initSrvKeys()
|
||||
super().__init__(listen_to, RequestHandler)
|
||||
|
||||
def initSrvKeys(self):
|
||||
fingerprint = self.db.getConfig("server_key")
|
||||
if not fingerprint:
|
||||
try:
|
||||
result = self.gpg_context.create_key(userid="PoC server", algorithm="ed25519",
|
||||
expires=False, sign=True)
|
||||
fingerprint = result.fpr
|
||||
result = self.gpg_context.create_subkey(key=self.gpg_context.get_key(fingerprint),
|
||||
algorithm="cv25519", expires=False, encrypt=True)
|
||||
self.db.setConfig("server_key", fingerprint)
|
||||
self.key = self.gpg_context.get_key(fingerprint)
|
||||
except gpg.errors.GPGMEError as error:
|
||||
print("Failed to create server key.")
|
||||
sysexit(error)
|
||||
except gpg.errors.KeyError as error:
|
||||
print("Can't find the key I just created... wtf?")
|
||||
sysexit(error)
|
||||
else:
|
||||
try:
|
||||
self.key = self.gpg_context.get_key(fingerprint)
|
||||
except gpg.errors.KeyError as error:
|
||||
print(f"Cannot find server key {fingerprint}.")
|
||||
sysexit(error)
|
||||
print(f"Loaded server key {fingerprint}.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
def run(listen_to, gpg_home, db):
|
||||
httpd = Server(listen_to, gpg_home, db)
|
||||
try:
|
||||
print(f"Starting server… listening to {listen_to[0]}:{listen_to[1]} with datadir {gpg_home}, db {db}")
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
print("Stopping server")
|
||||
httpd.server_close()
|
||||
|
||||
datadir = None
|
||||
host = "127.0.0.1"
|
||||
port = 8000
|
||||
if len(argv) > 3:
|
||||
datadir = argv[3]
|
||||
if len(argv) > 2:
|
||||
host = argv[2]
|
||||
if len(argv) > 1:
|
||||
port = int(argv[1])
|
||||
|
||||
if not datadir:
|
||||
with TemporaryDirectory(prefix="websocket-pgp-json") as tempdir:
|
||||
run((host, port), tempdir, ":memory:")
|
||||
else:
|
||||
gpg_home = os.path.join(datadir, "gpg")
|
||||
db = os.path.join(datadir, "sqlite.db")
|
||||
try:
|
||||
os.mkdir(gpg_home)
|
||||
except FileExistsError:
|
||||
print("Using existing GPG home dir.")
|
||||
run((host, port), gpg_home, db)
|
|
@ -0,0 +1,337 @@
|
|||
# References from
|
||||
# https://tools.ietf.org/html/rfc6455
|
||||
# https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
|
||||
# https://gist.github.com/SevenW/47be2f9ab74cac26bf21/ (SevenW/HTTPWebSocketsHandler.py)
|
||||
|
||||
from base64 import b64decode, b64encode
|
||||
import json
|
||||
from gpg import Context as GPGContext
|
||||
from gpg.errors import GPGMEError
|
||||
from hashlib import sha1
|
||||
from binascii import Error as BinasciiError
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from io import BufferedIOBase
|
||||
from time import time
|
||||
|
||||
_WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
||||
OPCODE = { "continueation": 0x0,
|
||||
"text": 0x1,
|
||||
"binary": 0x2,
|
||||
"close": 0x8,
|
||||
"ping": 0x9,
|
||||
"pong": 0xa }
|
||||
CONTROL_OPCODES = [OPCODE["close"], OPCODE["ping"], OPCODE["pong"]]
|
||||
NONCONTROL_OPCODES = [OPCODE["continueation"], OPCODE["text"], OPCODE["binary"]]
|
||||
PROTOCOL_VERSION = 13
|
||||
HTTP_VERSION = "HTTP/1.1"
|
||||
|
||||
PGP_JSON_SIGNATURE_VALIDITY = 60
|
||||
|
||||
class WebSocketException(Exception):
|
||||
pass
|
||||
|
||||
class FrameError(WebSocketException):
|
||||
def __init__(message: str, frame: dict):
|
||||
assert "FIN" in frame and frame["FIN"] is not None
|
||||
assert "RSV1" in frame and frame["RSV1"] is not None
|
||||
assert "RSV2" in frame and frame["RSV2"] is not None
|
||||
assert "RSV3" in frame and frame["RSV3"] is not None
|
||||
assert "opcode" in frame and frame["opcode"] is not None
|
||||
self.message = message
|
||||
self.frame = frame
|
||||
|
||||
class HandshakeError(WebSocketException):
|
||||
reason = { "invalid_header": 0,
|
||||
"missing_header": 1,
|
||||
"incompatible_version": 2 }
|
||||
|
||||
def __init__(self, what: str, value: str, why: int):
|
||||
assert why in self.reason.values()
|
||||
self.what = what
|
||||
self.value = value
|
||||
self.why = why
|
||||
|
||||
def get_reason(self, code: int):
|
||||
for k, v in self.reason.items():
|
||||
if v == code:
|
||||
return k
|
||||
|
||||
class InvalidLengthError(FrameError):
|
||||
def __init__(length: int):
|
||||
self.length = length
|
||||
|
||||
class WebSocketCloseException(WebSocketException):
|
||||
def __init__(self, code: int, reason: str):
|
||||
self.code = code
|
||||
self.reason = reason
|
||||
|
||||
class WebSocketMessage():
|
||||
def __add__(self, other):
|
||||
assert isinstance(other, WebSocketMessage)
|
||||
assert other["opcode"] == OPCODE["continueation"]
|
||||
if opcode == OPCODE["text"]:
|
||||
# may raise UnicodeError
|
||||
self.payload += other.payload.decode()
|
||||
else:
|
||||
self.payload += other.payload
|
||||
|
||||
def __init__(self, opcode, payload):
|
||||
self.opcode = opcode
|
||||
if opcode == OPCODE["text"]:
|
||||
# may raise UnicodeError
|
||||
self.payload = payload.decode()
|
||||
else:
|
||||
self.payload = payload
|
||||
|
||||
def __str__(self):
|
||||
return f"WebSocket Message opcode:0x{self.opcode:x} payload:{self.payload}"
|
||||
|
||||
def _check_required_headers(request_handler: BaseHTTPRequestHandler):
|
||||
if request_handler.request_version != HTTP_VERSION:
|
||||
request_handler.send_response(400)
|
||||
request_handler.end_headers()
|
||||
raise HandshakeError("HTTP", request_handler.request_version,
|
||||
HandshakeError.reason["incompatible_version"])
|
||||
|
||||
for header in [["Host", None],
|
||||
["Upgrade", "websocket"],
|
||||
["Connection", "upgrade"],
|
||||
["Sec-WebSocket-Key", None],
|
||||
["Sec-WebSocket-Version", None]]:
|
||||
value = request_handler.headers.get(header[0])
|
||||
requisite = header[1] in value.lower().split(", ") if header[1] is not None else value is not None
|
||||
reason = HandshakeError.reason["invalid_header"] if value is not None else HandshakeError.reason["missing_header"]
|
||||
if requisite is False:
|
||||
request_handler.send_response(400)
|
||||
request_handler.end_headers()
|
||||
raise HandshakeError(header[0], value, reason)
|
||||
|
||||
ws_key = request_handler.headers.get("Sec-WebSocket-Key")
|
||||
if ws_key is None:
|
||||
request_handler.send_response(400)
|
||||
request_handler.end_headers()
|
||||
raise HandshakeError("Sec-WebSocket-Key", None, HandshakeError.reason["missing_header"])
|
||||
try:
|
||||
invalid_key_error = HandshakeError("Sec-WebSocket-Key", ws_key, HandshakeError.reason["invalid_header"])
|
||||
decoded = b64decode(s=ws_key, altchars=None, validate=True)
|
||||
if len(decoded) != 16:
|
||||
request_handler.send_response(400)
|
||||
request_handler.end_headers()
|
||||
raise invalid_key_error
|
||||
except BinasciiError:
|
||||
request_handler.send_response(400)
|
||||
request_handler.end_headers()
|
||||
raise invalid_key_error
|
||||
|
||||
def _decode_payload(masking_key: bytes, encoded_payload: bytes):
|
||||
decoded_payload = bytearray()
|
||||
for byte in encoded_payload:
|
||||
decoded_payload += bytes([byte ^ masking_key[len(decoded_payload) % 4]])
|
||||
return decoded_payload
|
||||
|
||||
def _encode_data_frame(fin: int, opcode: int, rsv1: int, rsv2: int, rsv3: int, payload: bytes):
|
||||
assert isinstance(payload, bytes)
|
||||
if not opcode in OPCODE.values():
|
||||
raise ValueError(f"Unsupported opcode {opcode}. Valid values {list(OPCODE.values())}.")
|
||||
if fin == 0 and opcode in CONTROL_OPCODES:
|
||||
raise ValueError(f"Control frames cannot be fragmented")
|
||||
if opcode in CONTROL_OPCODES and len(payload) > 125:
|
||||
raise ValueError("Control frame cannot have a payload bigger than 125 bytes.")
|
||||
payload_length = len(payload)
|
||||
length_bits = 7
|
||||
if payload_length > 125:
|
||||
length_bits = 7 + 16
|
||||
elif payload_length > 0xffff:
|
||||
length_bits = 7 + 64
|
||||
elif payload_length > 0x7fffffffffffffff:
|
||||
raise ValueError(f"Payload maximal size exceeded (provided {payload_length} bytes).")
|
||||
frame_size = int(1 + (1 + length_bits) / 8 + payload_length)
|
||||
frame = bytearray(frame_size)
|
||||
frame[0] = (fin << 7) + (rsv1 << 6) + (rsv2 << 5) + (rsv3 << 4) + opcode
|
||||
if length_bits == 7:
|
||||
frame[1] = payload_length
|
||||
frame[2:] = payload
|
||||
elif length_bits == 7 + 16:
|
||||
frame[1] = 126
|
||||
frame[2:2] = payload_length.to_bytes(2, byteorder="big")
|
||||
frame[4:] = payload
|
||||
else:
|
||||
frame[1] = 127
|
||||
frame[2:8] = payload_length.to_bytes(8, byteorder="big")
|
||||
frame[11:] = payload
|
||||
|
||||
return frame
|
||||
|
||||
def _handle_control_frame(wfile: BufferedIOBase, frame: dict):
|
||||
if frame["opcode"] == OPCODE["close"]:
|
||||
code = frame["status_code"] if "status_code" in frame else None
|
||||
payload = code.to_bytes(2, byteorder="big") if code is not None else b""
|
||||
reason = frame["close_reason"] if "close_reason" in frame else "-"
|
||||
send_message(wfile, OPCODE["close"], payload)
|
||||
raise WebSocketCloseException(code, reason)
|
||||
elif frame["opcode"] == OPCODE["ping"]:
|
||||
payload = frame["payload"] if "payload" in frame else b""
|
||||
send_message(wfile, OPCODE["pong"], payload)
|
||||
|
||||
def _read_data_frame(rfile: BufferedIOBase):
|
||||
frame = {}
|
||||
#char = rfile.read(1)
|
||||
#if len(char) == 0:
|
||||
# return
|
||||
net_bytes = ord(rfile.read(1))
|
||||
frame["FIN"] = net_bytes >> 7
|
||||
frame["RSV1"] = (net_bytes & 0x40) >> 6
|
||||
frame["RSV2"] = (net_bytes & 0x20) >> 5
|
||||
frame["RSV3"] = (net_bytes & 0x10) >> 4
|
||||
frame["opcode"] = net_bytes & 0x0f
|
||||
|
||||
if frame["RSV1"] != 0 or frame["RSV2"] != 0 or frame["RSV3"] != 0:
|
||||
raise FrameError("Unsupported feature. RSV1, RSV2 or RSV3 has a non-zero value.", frame)
|
||||
|
||||
if not frame["opcode"] in OPCODE.values():
|
||||
raise FrameError("Unsupported opcode value.", frame)
|
||||
|
||||
if frame["FIN"] == 0 and frame["opcode"] != OPCODE["continueation"]:
|
||||
raise FrameError("FIN bit not set for a non-continueation frame.", frame)
|
||||
|
||||
if frame["opcode"] in CONTROL_OPCODES and frame["FIN"] == 0:
|
||||
raise FrameError("FIN bit not set for a control frame.", frame)
|
||||
|
||||
net_bytes = ord(rfile.read(1))
|
||||
mask_bit = net_bytes >> 7
|
||||
|
||||
if mask_bit == 0:
|
||||
raise FrameError("Unmasked frame from client.", frame)
|
||||
|
||||
length1 = net_bytes & 0x7f
|
||||
|
||||
if frame["opcode"] in CONTROL_OPCODES and length1 > 125:
|
||||
raise FrameError("Control frame with invalid payload length.", frame)
|
||||
|
||||
try:
|
||||
length = _read_payload_length(length1, rfile)
|
||||
except InvalidLengthError as error:
|
||||
raise FrameError(f"Invalid payload length of {error.length} bytes.", frame)
|
||||
|
||||
masking_key = rfile.read(4)
|
||||
encoded_payload = rfile.read(length)
|
||||
frame["payload"] = _decode_payload(masking_key, encoded_payload)
|
||||
|
||||
if frame["opcode"] == OPCODE["close"] and frame["payload"]:
|
||||
frame["status_code"] = int.from_bytes(frame["payload"][0:2], byteorder="big")
|
||||
if length > 2:
|
||||
# /!\ may raise UnicodeError /!\
|
||||
frame["close_reason"] = frame["payload"][2:].decode()
|
||||
|
||||
return frame
|
||||
|
||||
def _read_payload_length(payload_length1: int, rfile: BufferedIOBase):
|
||||
final_length = payload_length1
|
||||
if payload_length1 == 126:
|
||||
final_length = int.from_bytes(rfile.read(2), byteorder="big")
|
||||
elif payload_length1 == 127:
|
||||
final_length = int.from_bytes(rfile.read(8), byteorder="big")
|
||||
if final_length >> 63 == 1:
|
||||
raise InvalidLengthError(final_length)
|
||||
return final_length
|
||||
|
||||
def close(wfile: BufferedIOBase, code=1000, reason=None):
|
||||
code_bytes = code.to_bytes(2, byteorder="big")
|
||||
payload = code_bytes if reason is None else code_bytes + reason.encode()
|
||||
frame = _encode_data_frame(1, OPCODE["close"], 0, 0, 0, payload)
|
||||
wfile.write(frame)
|
||||
|
||||
def decrypt_pgp_json(wfile: BufferedIOBase, gpg_context: GPGContext, message: WebSocketMessage, signatures_keys=None):
|
||||
if message.opcode != OPCODE["text"]:
|
||||
close(wfile, 1003, "Only text datatype is allowed.")
|
||||
raise WebSocketCloseException(1003, "Received no-text datatype.")
|
||||
try:
|
||||
plaintext, result, verify_result = gpg_context.decrypt(message.payload.encode())
|
||||
except GPGMEError as error:
|
||||
close(wfile, 1002, f"Failed to decrypt message.")
|
||||
raise websocket.WebSocketCloseException(1002, f"Failed to decrypt websocket message: {error}.")
|
||||
|
||||
if len(verify_result.signatures) == 0:
|
||||
close(wfile, 1008, "No signature recognized.")
|
||||
raise WebSocketCloseException(1008, "Message with missing or unknown signature.")
|
||||
if signatures_keys is not None: # and client_key.fpr != verify_result.signatures[0].fpr:
|
||||
if len(signatures_keys) != len(verify_result.signatures):
|
||||
close(wfile, 1008, "Message signature check failed.")
|
||||
raise WebSocketCloseException(1008, "Signature count doesn't match signatures_keys")
|
||||
|
||||
signatures_fingerprints = [client_key.fpr for client_key in signatures_keys]
|
||||
message_signatures = [signature.fpr for signature in verify_result.signatures]
|
||||
for client_fpr in signatures_fingerprints:
|
||||
if client_fpr not in message_signatures:
|
||||
close(wfile, 1008, "Message signature check failed")
|
||||
raise WebSocketCloseException(1008, "Message has a signature from a key not in signatures_keys.")
|
||||
|
||||
signature_age = int(time()) - verify_result.signatures[0].timestamp
|
||||
if signature_age > PGP_JSON_SIGNATURE_VALIDITY:
|
||||
close(wfile, 1008, "Signature too old.")
|
||||
raise WebSocketCloseException(1008, f"Message with a {signature_age}s old signature.")
|
||||
|
||||
try:
|
||||
json_ = json.loads(plaintext)
|
||||
except json.JSONDecodeError:
|
||||
close(wfile, 1002, "Invalid JSON data.")
|
||||
raise WebSocketCloseException(1002, "Invalid JSON data.")
|
||||
|
||||
return json_, verify_result.signatures
|
||||
|
||||
def encrypt_pgp_json(obj: dict, recipients, gpg_context: GPGContext):
|
||||
json_ = json.dumps(obj).encode()
|
||||
ciphertext, result, sign_result = gpg_context.encrypt(json_,
|
||||
recipients=recipients,
|
||||
sign=True,
|
||||
always_trust=True)
|
||||
return ciphertext
|
||||
|
||||
def handshake(request_handler: BaseHTTPRequestHandler, subprotocols=[]):
|
||||
request_handler.protocol_version = HTTP_VERSION
|
||||
_check_required_headers(request_handler)
|
||||
websocket_key = request_handler.headers.get("Sec-WebSocket-Key")
|
||||
digest = b64encode(sha1((websocket_key + _WEBSOCKET_GUID).encode()).digest())
|
||||
websocket_version = request_handler.headers.get("Sec-WebSocket-Version")
|
||||
if int(websocket_version) != PROTOCOL_VERSION:
|
||||
request_handler.send_response(400)
|
||||
request_handler.send_header("Sec-WebSocket-Version", PROTOCOL_VERSION)
|
||||
request_handler.end_headers()
|
||||
raise HandshakeError("Sec-WebSocket-Version", websocket_version, HandshakeError.reason["incompatible_version"])
|
||||
selected_subprotocol = None
|
||||
requested_subprotocols = request_handler.headers.get("Sec-WebSocket-Protocol")
|
||||
if requested_subprotocols:
|
||||
requested_subprotocols = requested_subprotocols.split(",")
|
||||
for proto in requested_subprotocols:
|
||||
for serv_proto in subprotocols:
|
||||
if proto == serv_proto:
|
||||
selected_subprotocol = proto
|
||||
|
||||
request_handler.send_response(101)
|
||||
request_handler.send_header("Upgrade", "websocket")
|
||||
request_handler.send_header("Connection", "Upgrade")
|
||||
request_handler.send_header("Sec-WebSocket-Accept", digest.decode())
|
||||
if requested_subprotocols and selected_subprotocol is not None:
|
||||
request_handler.send_header("Sec-WebSocket-Protocol", selected_subprotocol)
|
||||
request_handler.end_headers()
|
||||
|
||||
return selected_subprotocol
|
||||
|
||||
# TODO make it iterative
|
||||
def read_next_message(rfile: BufferedIOBase, wfile: BufferedIOBase):
|
||||
frame = _read_data_frame(rfile)
|
||||
message = WebSocketMessage(frame["opcode"], frame["payload"])
|
||||
if frame["FIN"] == 1:
|
||||
if frame["opcode"] in NONCONTROL_OPCODES:
|
||||
return message
|
||||
else:
|
||||
_handle_control_frame(wfile, frame)
|
||||
return read_next_message(rfile, wfile)
|
||||
else:
|
||||
return message + read_next_message(rfile)
|
||||
|
||||
def send_message(wfile: BufferedIOBase, payload: bytes, opcode=OPCODE["text"], rsv1=0, rsv2=0, rsv3=0):
|
||||
if len(payload) > 0x7fffffffffffffff:
|
||||
raise ValueError(f"Payload to big. Sending fragmented messages not implemented.")
|
||||
frame = _encode_data_frame(1, opcode, rsv1, rsv2, rsv3, payload)
|
||||
wfile.write(frame)
|
Loading…
Reference in New Issue