v14: persistent E2E keys - browser localStorage + server keys.json
Browser: - ECDH key pair saved to localStorage (chat-key-priv, chat-key-pub) - Loaded on reconnect, only generated once - Re-registers public key with server on every connect - Corrupted keys auto-regenerate Server: - Keys saved to keys.json on disk after each registration - Loaded on startup, survives restarts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
64
chat.py
64
chat.py
@@ -25,7 +25,7 @@ import html
|
||||
import urllib.parse
|
||||
|
||||
PORT = 9999
|
||||
VERSION = "13"
|
||||
VERSION = "14"
|
||||
TUNNEL_TARGETS = {
|
||||
"parspack": ("185.208.174.152", 22),
|
||||
"mequ": ("188.213.68.133", 2022),
|
||||
@@ -37,8 +37,29 @@ MAX_TOTAL_STORAGE = 50 * 1024 * 1024 # 50 MB total
|
||||
# ── Server ──────────────────────────────────────────────────────────────
|
||||
|
||||
# E2E encryption: public key registry (username -> JWK public key JSON string)
|
||||
user_keys: dict[str, str] = {}
|
||||
# DM routing: username -> list of (group, queue) for SSE, or (group, writer) for TCP
|
||||
KEYS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys.json")
|
||||
|
||||
|
||||
def load_keys() -> dict[str, str]:
|
||||
"""Load user keys from disk."""
|
||||
try:
|
||||
with open(KEYS_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return {}
|
||||
|
||||
|
||||
def save_keys():
|
||||
"""Persist user keys to disk."""
|
||||
try:
|
||||
with open(KEYS_FILE, "w") as f:
|
||||
json.dump(user_keys, f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
user_keys: dict[str, str] = load_keys()
|
||||
# DM routing: username -> list of queues for SSE delivery
|
||||
dm_targets: dict[str, list] = {}
|
||||
|
||||
uploaded_files: dict[str, bytes] = {} # file_id -> raw bytes (insertion order)
|
||||
@@ -477,11 +498,37 @@ let myPubJwk = null;
|
||||
const derivedKeys = {}; // cache: username -> CryptoKey (AES)
|
||||
|
||||
async function initCrypto() {
|
||||
myKeyPair = await crypto.subtle.generateKey(
|
||||
{ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']
|
||||
);
|
||||
myPubJwk = await crypto.subtle.exportKey('jwk', myKeyPair.publicKey);
|
||||
// Register our public key with the server
|
||||
// Try to load persisted keys from localStorage
|
||||
const savedPriv = localStorage.getItem('chat-key-priv');
|
||||
const savedPub = localStorage.getItem('chat-key-pub');
|
||||
if (savedPriv && savedPub) {
|
||||
try {
|
||||
const privJwk = JSON.parse(savedPriv);
|
||||
myPubJwk = JSON.parse(savedPub);
|
||||
const privKey = await crypto.subtle.importKey(
|
||||
'jwk', privJwk, { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']
|
||||
);
|
||||
const pubKey = await crypto.subtle.importKey(
|
||||
'jwk', myPubJwk, { name: 'ECDH', namedCurve: 'P-256' }, true, []
|
||||
);
|
||||
myKeyPair = { privateKey: privKey, publicKey: pubKey };
|
||||
} catch(e) {
|
||||
// Corrupted keys, regenerate
|
||||
localStorage.removeItem('chat-key-priv');
|
||||
localStorage.removeItem('chat-key-pub');
|
||||
return initCrypto();
|
||||
}
|
||||
} else {
|
||||
// Generate new keys and persist
|
||||
myKeyPair = await crypto.subtle.generateKey(
|
||||
{ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']
|
||||
);
|
||||
myPubJwk = await crypto.subtle.exportKey('jwk', myKeyPair.publicKey);
|
||||
const privJwk = await crypto.subtle.exportKey('jwk', myKeyPair.privateKey);
|
||||
localStorage.setItem('chat-key-priv', JSON.stringify(privJwk));
|
||||
localStorage.setItem('chat-key-pub', JSON.stringify(myPubJwk));
|
||||
}
|
||||
// Always register public key with server (re-registers on reconnect)
|
||||
const myName = $name.value.trim() || 'anon';
|
||||
fetch('/keys', {
|
||||
method: 'POST',
|
||||
@@ -1000,6 +1047,7 @@ async def handle_http(reader, writer, first_line):
|
||||
key = params.get("key", [""])[0]
|
||||
if name and key:
|
||||
user_keys[name] = key
|
||||
save_keys()
|
||||
writer.write(b"HTTP/1.1 204 No Content\r\n\r\n")
|
||||
await writer.drain()
|
||||
writer.close()
|
||||
|
||||
Reference in New Issue
Block a user