Encrypt remote transport with post-quantum session keys
This commit is contained in:
@@ -29,17 +29,25 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth
|
||||
from browser_cli.client import _resolve_socket, BrowserNotConnected
|
||||
from browser_cli.platform import is_windows
|
||||
|
||||
response_secret = None
|
||||
|
||||
def _send_payload(data: bytes) -> None:
|
||||
if response_secret is not None:
|
||||
from browser_cli.auth import pq_encrypt
|
||||
data = json.dumps({"encrypted": pq_encrypt(response_secret, "response", data)}).encode()
|
||||
_framed_send(client_sock, data)
|
||||
|
||||
def _send_error(msg_id, msg:str) -> None:
|
||||
err = json.dumps({"id": msg_id, "success": False, "error": msg}).encode()
|
||||
try:
|
||||
_framed_send(client_sock, err)
|
||||
_send_payload(err)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _send_ok(msg_id, payload) -> None:
|
||||
out = json.dumps({"id": msg_id, "success": True, "data": payload}).encode()
|
||||
try:
|
||||
_framed_send(client_sock, out)
|
||||
_send_payload(out)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
@@ -93,9 +101,10 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth
|
||||
_log(addr, command, None, "DENIED", "untrusted key")
|
||||
return
|
||||
pq_shared_secret = None
|
||||
transport_encrypted = False
|
||||
if pq_private_key is not None:
|
||||
kex = msg.get("pq_kex") or {}
|
||||
pq_required = parse_version(client_ver) >= parse_version("0.9.4")
|
||||
pq_required = parse_version(client_ver) >= parse_version("0.9.5")
|
||||
if not isinstance(kex, dict) or kex.get("alg") != "ML-KEM-768" or not kex.get("ciphertext"):
|
||||
if pq_required:
|
||||
_send_error(msg_id, "unauthorized: post-quantum key exchange required")
|
||||
@@ -103,11 +112,26 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth
|
||||
return
|
||||
else:
|
||||
try:
|
||||
from browser_cli.auth import pq_kex_server_decapsulate
|
||||
from browser_cli.auth import pq_decrypt, pq_kex_server_decapsulate
|
||||
pq_shared_secret = pq_kex_server_decapsulate(pq_private_key, str(kex["ciphertext"]))
|
||||
if "encrypted" in msg:
|
||||
decrypted_msg = json.loads(pq_decrypt(pq_shared_secret, "request", msg["encrypted"]))
|
||||
if not isinstance(decrypted_msg, dict):
|
||||
raise ValueError("encrypted request is not a JSON object")
|
||||
decrypted_msg["pubkey"] = pub
|
||||
decrypted_msg["sig"] = sig
|
||||
decrypted_msg["pq_kex"] = kex
|
||||
msg = adapt_auth(decrypted_msg, client_ver)
|
||||
msg_id = msg.get("id", msg_id)
|
||||
command = msg.get("command", "?")
|
||||
transport_encrypted = True
|
||||
elif pq_required:
|
||||
_send_error(msg_id, "unauthorized: post-quantum encrypted transport required")
|
||||
_log(addr, command, None, "DENIED", "missing pq transport")
|
||||
return
|
||||
except Exception:
|
||||
_send_error(msg_id, "unauthorized: invalid post-quantum key exchange")
|
||||
_log(addr, command, None, "DENIED", "bad pq kex")
|
||||
_send_error(msg_id, "unauthorized: invalid post-quantum encrypted transport")
|
||||
_log(addr, command, None, "DENIED", "bad pq transport")
|
||||
return
|
||||
|
||||
from browser_cli.auth import verify
|
||||
@@ -115,6 +139,7 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth
|
||||
_send_error(msg_id, "unauthorized: invalid signature")
|
||||
_log(addr, command, None, "DENIED", "bad signature")
|
||||
return
|
||||
response_secret = pq_shared_secret if transport_encrypted else None
|
||||
|
||||
if command == "browser-cli.targets":
|
||||
from browser_cli.client import active_browser_targets
|
||||
@@ -158,7 +183,7 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth
|
||||
resolved_profile = msg.get("_route") or profile
|
||||
|
||||
# ── strip protocol fields, apply request compat shim, forward ─────────────
|
||||
strip = {"token", "_route", "pubkey", "sig", "user_agent", "pq_kex"}
|
||||
strip = {"token", "_route", "pubkey", "sig", "user_agent", "pq_kex", "encrypted"}
|
||||
clean_msg = {k: v for k, v in msg.items() if k not in strip}
|
||||
clean_msg = adapt_request(clean_msg, client_ver)
|
||||
clean_payload = json.dumps(clean_msg).encode()
|
||||
@@ -178,14 +203,14 @@ def _proxy_request(client_sock:socket.socket, addr:tuple, profile:str|None, auth
|
||||
pipe.send_bytes(clean_payload)
|
||||
resp_payload = pipe.recv_bytes()
|
||||
resp_payload = adapt_response(resp_payload, command, client_ver)
|
||||
_framed_send(client_sock, resp_payload)
|
||||
_send_payload(resp_payload)
|
||||
else:
|
||||
with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as local:
|
||||
local.connect(sock_path)
|
||||
local.sendall(clean_header + clean_payload)
|
||||
resp_payload = _recv_all(local)
|
||||
resp_payload = adapt_response(resp_payload, command, client_ver)
|
||||
_framed_send(client_sock, resp_payload)
|
||||
_send_payload(resp_payload)
|
||||
|
||||
resp_data = json.loads(resp_payload)
|
||||
if resp_data.get("success", True):
|
||||
|
||||
Reference in New Issue
Block a user