initial commit with WiP code

This commit is contained in:
soratobuneko 2021-01-01 12:40:42 +01:00
commit c57f762789
8 changed files with 996 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
__pycache__
openpgp.min.js

31
admin.html Normal file
View File

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

4
config.js Normal file
View File

@ -0,0 +1,4 @@
"use strict"
const SRV = "127.0.0.1:8000"
const FETCH_PROTOCOL = "http"

62
database.py Normal file
View File

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

121
register.html Normal file
View File

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

138
registered.js Normal file
View File

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

301
server.py Executable file
View File

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

337
websocket.py Normal file
View File

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