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
|
import urllib.parse
|
||||||
|
|
||||||
PORT = 9999
|
PORT = 9999
|
||||||
VERSION = "13"
|
VERSION = "14"
|
||||||
TUNNEL_TARGETS = {
|
TUNNEL_TARGETS = {
|
||||||
"parspack": ("185.208.174.152", 22),
|
"parspack": ("185.208.174.152", 22),
|
||||||
"mequ": ("188.213.68.133", 2022),
|
"mequ": ("188.213.68.133", 2022),
|
||||||
@@ -37,8 +37,29 @@ MAX_TOTAL_STORAGE = 50 * 1024 * 1024 # 50 MB total
|
|||||||
# ── Server ──────────────────────────────────────────────────────────────
|
# ── Server ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# E2E encryption: public key registry (username -> JWK public key JSON string)
|
# E2E encryption: public key registry (username -> JWK public key JSON string)
|
||||||
user_keys: dict[str, str] = {}
|
KEYS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "keys.json")
|
||||||
# DM routing: username -> list of (group, queue) for SSE, or (group, writer) for TCP
|
|
||||||
|
|
||||||
|
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] = {}
|
dm_targets: dict[str, list] = {}
|
||||||
|
|
||||||
uploaded_files: dict[str, bytes] = {} # file_id -> raw bytes (insertion order)
|
uploaded_files: dict[str, bytes] = {} # file_id -> raw bytes (insertion order)
|
||||||
@@ -477,11 +498,37 @@ let myPubJwk = null;
|
|||||||
const derivedKeys = {}; // cache: username -> CryptoKey (AES)
|
const derivedKeys = {}; // cache: username -> CryptoKey (AES)
|
||||||
|
|
||||||
async function initCrypto() {
|
async function initCrypto() {
|
||||||
myKeyPair = await crypto.subtle.generateKey(
|
// Try to load persisted keys from localStorage
|
||||||
{ name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits']
|
const savedPriv = localStorage.getItem('chat-key-priv');
|
||||||
);
|
const savedPub = localStorage.getItem('chat-key-pub');
|
||||||
myPubJwk = await crypto.subtle.exportKey('jwk', myKeyPair.publicKey);
|
if (savedPriv && savedPub) {
|
||||||
// Register our public key with the server
|
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';
|
const myName = $name.value.trim() || 'anon';
|
||||||
fetch('/keys', {
|
fetch('/keys', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -1000,6 +1047,7 @@ async def handle_http(reader, writer, first_line):
|
|||||||
key = params.get("key", [""])[0]
|
key = params.get("key", [""])[0]
|
||||||
if name and key:
|
if name and key:
|
||||||
user_keys[name] = key
|
user_keys[name] = key
|
||||||
|
save_keys()
|
||||||
writer.write(b"HTTP/1.1 204 No Content\r\n\r\n")
|
writer.write(b"HTTP/1.1 204 No Content\r\n\r\n")
|
||||||
await writer.drain()
|
await writer.drain()
|
||||||
writer.close()
|
writer.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user