From 1d42a9a6237ecb28e57ac0afc1d2b021dea5f192 Mon Sep 17 00:00:00 2001 From: doomtube Date: Wed, 13 Aug 2025 00:10:25 -0400 Subject: [PATCH] nu --- backend/Dockerfile | 10 +- backend/src/controllers/UserController.cpp | 83 +- backend/src/controllers/UserController.h | 4 + backend/src/services/AuthService.cpp | 183 ++- frontend/src/lib/pgp.js | 233 +++- frontend/src/lib/stores/auth.js | 106 +- frontend/src/routes/login/+page.svelte | 211 ++- frontend/src/routes/settings/+page.svelte | 436 ++++-- text.txt | 1398 +++++++++++++++----- 9 files changed, 2122 insertions(+), 542 deletions(-) diff --git a/backend/Dockerfile b/backend/Dockerfile index 5541565..83bc201 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,7 +2,7 @@ FROM drogonframework/drogon:latest WORKDIR /app -# Install additional dependencies including redis-plus-plus dev package if available +# Install additional dependencies including GPG for PGP verification RUN apt-get update && apt-get install -y \ libpq-dev \ postgresql-client \ @@ -12,6 +12,8 @@ RUN apt-get update && apt-get install -y \ libhiredis-dev \ curl \ libssl-dev \ + gnupg \ + gnupg2 \ && rm -rf /var/lib/apt/lists/* # Try to install redis-plus-plus from package manager first @@ -87,6 +89,10 @@ RUN mkdir -p /app/uploads/avatars && \ chown -R 65534:65534 /app/uploads && \ chmod -R 755 /app/uploads +# Create a temporary directory for GPG operations +RUN mkdir -p /tmp/pgp_verify && \ + chmod 777 /tmp/pgp_verify + # Ensure libraries are available ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH @@ -94,6 +100,8 @@ ENV LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH RUN echo '#!/bin/bash\n\ echo "Checking library dependencies..."\n\ ldd ./build/streaming-backend\n\ +echo "Checking GPG installation..."\n\ +gpg --version\n\ echo "Ensuring upload directories exist with proper permissions..."\n\ mkdir -p /app/uploads/avatars\n\ chown -R 65534:65534 /app/uploads\n\ diff --git a/backend/src/controllers/UserController.cpp b/backend/src/controllers/UserController.cpp index ffe96e9..b0d19c4 100644 --- a/backend/src/controllers/UserController.cpp +++ b/backend/src/controllers/UserController.cpp @@ -1,6 +1,7 @@ #include "UserController.h" #include "../services/DatabaseService.h" #include +#include #include #include #include @@ -51,18 +52,46 @@ namespace { return false; } } + + // Helper to set httpOnly auth cookie + void setAuthCookie(const HttpResponsePtr& resp, const std::string& token) { + Cookie authCookie("auth_token", token); + authCookie.setPath("/"); + authCookie.setHttpOnly(true); + authCookie.setSecure(false); // Set to true in production with HTTPS + authCookie.setMaxAge(86400); // 24 hours + authCookie.setSameSite(Cookie::SameSite::kLax); + resp->addCookie(authCookie); + } + + // Helper to clear auth cookie + void clearAuthCookie(const HttpResponsePtr& resp) { + Cookie authCookie("auth_token", ""); + authCookie.setPath("/"); + authCookie.setHttpOnly(true); + authCookie.setMaxAge(0); // Expire immediately + resp->addCookie(authCookie); + } } UserInfo UserController::getUserFromRequest(const HttpRequestPtr &req) { UserInfo user; - std::string auth = req->getHeader("Authorization"); - if (auth.empty() || auth.substr(0, 7) != "Bearer ") { - return user; + // First try to get from cookie + std::string token = req->getCookie("auth_token"); + + // Fallback to Authorization header for API clients + if (token.empty()) { + std::string auth = req->getHeader("Authorization"); + if (!auth.empty() && auth.substr(0, 7) == "Bearer ") { + token = auth.substr(7); + } + } + + if (!token.empty()) { + AuthService::getInstance().validateToken(token, user); } - std::string token = auth.substr(7); - AuthService::getInstance().validateToken(token, user); return user; } @@ -168,9 +197,10 @@ void UserController::login(const HttpRequestPtr &req, [callback, username](bool success, const std::string& token, const UserInfo& user) { if (success) { LOG_INFO << "Login successful for user: " << username; + Json::Value resp; resp["success"] = true; - resp["token"] = token; + // Don't send token in body for cookie-based auth resp["user"]["id"] = static_cast(user.id); resp["user"]["username"] = user.username; resp["user"]["isAdmin"] = user.isAdmin; @@ -179,8 +209,11 @@ void UserController::login(const HttpRequestPtr &req, resp["user"]["bio"] = user.bio; resp["user"]["avatarUrl"] = user.avatarUrl; resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt; - resp["user"]["colorCode"] = user.colorCode; // Use colorCode for consistency with database field name - callback(jsonResp(resp)); + resp["user"]["colorCode"] = user.colorCode; + + auto response = jsonResp(resp); + setAuthCookie(response, token); + callback(response); } else { LOG_WARN << "Login failed for user: " << username; callback(jsonError(token.empty() ? "Invalid credentials" : token, k401Unauthorized)); @@ -195,6 +228,22 @@ void UserController::login(const HttpRequestPtr &req, } } +void UserController::logout(const HttpRequestPtr &req, + std::function &&callback) { + try { + Json::Value resp; + resp["success"] = true; + resp["message"] = "Logged out successfully"; + + auto response = jsonResp(resp); + clearAuthCookie(response); + callback(response); + } catch (const std::exception& e) { + LOG_ERROR << "Exception in logout: " << e.what(); + callback(jsonError("Internal server error")); + } +} + void UserController::pgpChallenge(const HttpRequestPtr &req, std::function &&callback) { try { @@ -264,7 +313,7 @@ void UserController::pgpVerify(const HttpRequestPtr &req, if (success) { Json::Value resp; resp["success"] = true; - resp["token"] = token; + // Don't send token in body for cookie-based auth resp["user"]["id"] = static_cast(user.id); resp["user"]["username"] = user.username; resp["user"]["isAdmin"] = user.isAdmin; @@ -273,8 +322,11 @@ void UserController::pgpVerify(const HttpRequestPtr &req, resp["user"]["bio"] = user.bio; resp["user"]["avatarUrl"] = user.avatarUrl; resp["user"]["pgpOnlyEnabledAt"] = user.pgpOnlyEnabledAt; - resp["user"]["colorCode"] = user.colorCode; // Add colorCode to PGP login response - callback(jsonResp(resp)); + resp["user"]["colorCode"] = user.colorCode; + + auto response = jsonResp(resp); + setAuthCookie(response, token); + callback(response); } else { callback(jsonError("Invalid signature", k401Unauthorized)); } @@ -782,7 +834,7 @@ void UserController::updateColor(const HttpRequestPtr &req, AuthService::getInstance().updateUserColor(user.id, newColor, [callback, user](bool success, const std::string& error, const std::string& finalColor) { if (success) { - // Fetch updated user info and generate new token with updated color + // Fetch updated user info AuthService::getInstance().fetchUserInfo(user.id, [callback, finalColor](bool fetchSuccess, const UserInfo& updatedUser) { if (fetchSuccess) { @@ -792,13 +844,16 @@ void UserController::updateColor(const HttpRequestPtr &req, Json::Value resp; resp["success"] = true; resp["color"] = finalColor; - resp["token"] = newToken; // Return new token with updated color resp["user"]["id"] = static_cast(updatedUser.id); resp["user"]["username"] = updatedUser.username; resp["user"]["isAdmin"] = updatedUser.isAdmin; resp["user"]["isStreamer"] = updatedUser.isStreamer; resp["user"]["colorCode"] = updatedUser.colorCode; - callback(jsonResp(resp)); + + auto response = jsonResp(resp); + // Update auth cookie with new token + setAuthCookie(response, newToken); + callback(response); } else { // Color was updated but couldn't fetch full user info Json::Value resp; diff --git a/backend/src/controllers/UserController.h b/backend/src/controllers/UserController.h index 8580259..cf23349 100644 --- a/backend/src/controllers/UserController.h +++ b/backend/src/controllers/UserController.h @@ -9,6 +9,7 @@ public: METHOD_LIST_BEGIN ADD_METHOD_TO(UserController::register_, "/api/auth/register", Post); ADD_METHOD_TO(UserController::login, "/api/auth/login", Post); + ADD_METHOD_TO(UserController::logout, "/api/auth/logout", Post); ADD_METHOD_TO(UserController::pgpChallenge, "/api/auth/pgp-challenge", Post); ADD_METHOD_TO(UserController::pgpVerify, "/api/auth/pgp-verify", Post); ADD_METHOD_TO(UserController::getCurrentUser, "/api/user/me", Get); @@ -30,6 +31,9 @@ public: void login(const HttpRequestPtr &req, std::function &&callback); + void logout(const HttpRequestPtr &req, + std::function &&callback); + void pgpChallenge(const HttpRequestPtr &req, std::function &&callback); diff --git a/backend/src/services/AuthService.cpp b/backend/src/services/AuthService.cpp index 1fb6a6d..9933ca8 100644 --- a/backend/src/services/AuthService.cpp +++ b/backend/src/services/AuthService.cpp @@ -6,6 +6,9 @@ #include #include #include +#include +#include +#include using namespace drogon; using namespace drogon::orm; @@ -29,6 +32,159 @@ bool AuthService::validatePassword(const std::string& password, std::string& err return true; } +// Helper function to execute GPG commands +std::string executeGpgCommand(const std::string& command) { + std::array buffer; + std::string result; + + FILE* pipe = popen(command.c_str(), "r"); + if (!pipe) { + LOG_ERROR << "Failed to execute GPG command: " << command; + return ""; + } + + while (fgets(buffer.data(), buffer.size(), pipe) != nullptr) { + result += buffer.data(); + } + + int exitCode = pclose(pipe); + + // Exit code is returned as status << 8, so we need to extract the actual exit code + int actualExitCode = WEXITSTATUS(exitCode); + + if (actualExitCode != 0) { + LOG_ERROR << "GPG command failed with exit code: " << actualExitCode + << " for command: " << command + << " output: " << result; + // Don't return empty string immediately - sometimes GPG returns non-zero but still works + } + + return result; +} + +// Server-side PGP signature verification +bool verifyPgpSignature(const std::string& message, const std::string& signature, const std::string& publicKey) { + try { + // Create temporary directory for GPG operations + std::string tmpDir = "/tmp/pgp_verify_" + drogon::utils::genRandomString(8); + std::string mkdirCmd = "mkdir -p " + tmpDir; + if (system(mkdirCmd.c_str()) != 0) { + LOG_ERROR << "Failed to create temporary directory: " << tmpDir; + return false; + } + + // Create GPG home directory + std::string keyringDir = tmpDir + "/gnupg"; + std::string mkdirGpgCmd = "mkdir -p " + keyringDir + " && chmod 700 " + keyringDir; + if (system(mkdirGpgCmd.c_str()) != 0) { + LOG_ERROR << "Failed to create GPG home directory: " << keyringDir; + std::string cleanupCmd = "rm -rf " + tmpDir; + system(cleanupCmd.c_str()); + return false; + } + + // Write files + std::string messageFile = tmpDir + "/message.txt"; + std::string sigFile = tmpDir + "/signature.asc"; + std::string pubkeyFile = tmpDir + "/pubkey.asc"; + + // Write message file + std::ofstream msgOut(messageFile); + if (!msgOut) { + LOG_ERROR << "Failed to create message file: " << messageFile; + std::string cleanupCmd = "rm -rf " + tmpDir; + system(cleanupCmd.c_str()); + return false; + } + msgOut << message; + msgOut.close(); + + // Write signature file + std::ofstream sigOut(sigFile); + if (!sigOut) { + LOG_ERROR << "Failed to create signature file: " << sigFile; + std::string cleanupCmd = "rm -rf " + tmpDir; + system(cleanupCmd.c_str()); + return false; + } + sigOut << signature; + sigOut.close(); + + // Write public key file + std::ofstream keyOut(pubkeyFile); + if (!keyOut) { + LOG_ERROR << "Failed to create public key file: " << pubkeyFile; + std::string cleanupCmd = "rm -rf " + tmpDir; + system(cleanupCmd.c_str()); + return false; + } + keyOut << publicKey; + keyOut.close(); + + // Initialize GPG (create trustdb if needed) + std::string initCmd = "GNUPGHOME=" + keyringDir + " gpg --batch --yes --list-keys 2>&1"; + executeGpgCommand(initCmd); // This will create the trustdb if it doesn't exist + + // Import the public key to the temporary keyring + // Use --trust-model always to avoid trust issues + std::string importCmd = "GNUPGHOME=" + keyringDir + + " gpg --batch --yes --trust-model always --import " + pubkeyFile + " 2>&1"; + std::string importResult = executeGpgCommand(importCmd); + + LOG_DEBUG << "GPG import result: " << importResult; + + // Check if import was successful (be more lenient with the check) + bool importSuccess = (importResult.find("imported") != std::string::npos) || + (importResult.find("unchanged") != std::string::npos) || + (importResult.find("processed: 1") != std::string::npos) || + (importResult.find("public key") != std::string::npos); + + if (!importSuccess) { + LOG_ERROR << "Failed to import public key. Import output: " << importResult; + // Try to get more information about what went wrong + std::string debugCmd = "GNUPGHOME=" + keyringDir + " gpg --list-keys 2>&1"; + std::string debugResult = executeGpgCommand(debugCmd); + LOG_ERROR << "GPG keyring state: " << debugResult; + + // Cleanup + std::string cleanupCmd = "rm -rf " + tmpDir; + system(cleanupCmd.c_str()); + return false; + } + + // Verify the signature + // Use --trust-model always to avoid trust issues + std::string verifyCmd = "GNUPGHOME=" + keyringDir + + " gpg --batch --yes --trust-model always --verify " + + sigFile + " " + messageFile + " 2>&1"; + std::string verifyResult = executeGpgCommand(verifyCmd); + + LOG_DEBUG << "GPG verify result: " << verifyResult; + + // Check if verification succeeded (check both English and potential localized messages) + bool verified = (verifyResult.find("Good signature") != std::string::npos) || + (verifyResult.find("gpg: Good signature") != std::string::npos) || + (verifyResult.find("Signature made") != std::string::npos && + verifyResult.find("BAD signature") == std::string::npos); + + if (!verified) { + LOG_WARN << "Signature verification failed. Verify output: " << verifyResult; + } else { + LOG_INFO << "Signature verification successful for challenge"; + } + + // Cleanup temporary files + std::string cleanupCmd = "rm -rf " + tmpDir; + system(cleanupCmd.c_str()); + + return verified; + + } catch (const std::exception& e) { + LOG_ERROR << "Exception during signature verification: " << e.what(); + return false; + } +} + void AuthService::generateUniqueColor(std::function callback) { try { auto dbClient = app().getDbClient(); @@ -421,6 +577,7 @@ void AuthService::verifyPgpLogin(const std::string& username, const std::string& [username, signature, challenge, callback, this](const std::string& storedChallenge) { try { if (storedChallenge.empty() || storedChallenge != challenge) { + LOG_WARN << "Challenge mismatch for user: " << username; callback(false, "", UserInfo{}); return; } @@ -428,9 +585,7 @@ void AuthService::verifyPgpLogin(const std::string& username, const std::string& // Delete challenge after use RedisHelper::deleteKeyAsync("pgp_challenge:" + username, [](bool) {}); - // In a real implementation, you would verify the signature here - // For now, we'll trust the client-side verification - + // Get user's public key and verify signature auto dbClient = app().getDbClient(); if (!dbClient) { LOG_ERROR << "Database client is null"; @@ -438,16 +593,32 @@ void AuthService::verifyPgpLogin(const std::string& username, const std::string& return; } - *dbClient << "SELECT id, username, is_admin, is_streamer, is_pgp_only, bio, avatar_url, pgp_only_enabled_at, user_color " - "FROM users WHERE username = $1 LIMIT 1" + *dbClient << "SELECT pk.public_key, u.id, u.username, u.is_admin, u.is_streamer, " + "u.is_pgp_only, u.bio, u.avatar_url, u.pgp_only_enabled_at, u.user_color " + "FROM pgp_keys pk JOIN users u ON pk.user_id = u.id " + "WHERE u.username = $1 ORDER BY pk.created_at DESC LIMIT 1" << username - >> [callback, this](const Result& r) { + >> [callback, signature, challenge, this](const Result& r) { try { if (r.empty()) { + LOG_WARN << "No PGP key found for user"; callback(false, "", UserInfo{}); return; } + std::string publicKey = r[0]["public_key"].as(); + + // CRITICAL: Server-side signature verification + bool signatureValid = verifyPgpSignature(challenge, signature, publicKey); + + if (!signatureValid) { + LOG_WARN << "Invalid PGP signature for user"; + callback(false, "Invalid signature", UserInfo{}); + return; + } + + LOG_INFO << "PGP signature verified successfully for user"; + UserInfo user; user.id = r[0]["id"].as(); user.username = r[0]["username"].as(); diff --git a/frontend/src/lib/pgp.js b/frontend/src/lib/pgp.js index 061aee2..2b35eb2 100644 --- a/frontend/src/lib/pgp.js +++ b/frontend/src/lib/pgp.js @@ -1,17 +1,89 @@ -// Client-side PGP utilities - wraps openpgp for browser-only usage +// Client-side PGP utilities with encrypted storage -export async function generateKeyPair(username, passphrase = '') { +const DB_NAME = 'pgp_storage'; +const DB_VERSION = 1; +const STORE_NAME = 'encrypted_keys'; + +// Initialize IndexedDB +async function initDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); + + request.onupgradeneeded = (event) => { + const db = event.target.result; + if (!db.objectStoreNames.contains(STORE_NAME)) { + db.createObjectStore(STORE_NAME, { keyPath: 'id' }); + } + }; + }); +} + +// Derive key from passphrase using PBKDF2 +async function deriveKey(passphrase, salt) { + const enc = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + 'raw', + enc.encode(passphrase), + 'PBKDF2', + false, + ['deriveBits', 'deriveKey'] + ); + + return crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt: salt, + iterations: 100000, + hash: 'SHA-256' + }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + true, + ['encrypt', 'decrypt'] + ); +} + +// Validate passphrase strength +export function validatePassphrase(passphrase) { + if (!passphrase || passphrase.length < 12) { + return 'Passphrase must be at least 12 characters'; + } + + // Check for complexity + const hasUpper = /[A-Z]/.test(passphrase); + const hasLower = /[a-z]/.test(passphrase); + const hasNumber = /[0-9]/.test(passphrase); + const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(passphrase); + + const complexity = [hasUpper, hasLower, hasNumber, hasSpecial].filter(Boolean).length; + if (complexity < 3) { + return 'Passphrase must contain at least 3 of: uppercase, lowercase, numbers, special characters'; + } + + return null; // Valid +} + +export async function generateKeyPair(username, passphrase) { if (typeof window === 'undefined') { throw new Error('PGP operations can only be performed in the browser'); } + // Validate passphrase + const passphraseError = validatePassphrase(passphrase); + if (passphraseError) { + throw new Error(passphraseError); + } + const { generateKey, readKey } = await import('openpgp'); const { privateKey, publicKey } = await generateKey({ type: 'rsa', rsaBits: 2048, userIDs: [{ name: username }], - passphrase + passphrase // Always encrypt with passphrase }); const key = await readKey({ armoredKey: publicKey }); @@ -37,11 +109,152 @@ export async function getFingerprint(publicKey) { } } -export async function signMessage(message, privateKeyArmored, passphrase = '') { +// Save encrypted private key to IndexedDB +export async function saveEncryptedPrivateKey(passphrase, armoredKey) { + if (typeof window === 'undefined') { + throw new Error('Storage operations can only be performed in the browser'); + } + + // Validate passphrase + const passphraseError = validatePassphrase(passphrase); + if (passphraseError) { + throw new Error(passphraseError); + } + + try { + // Generate random salt and IV + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + + // Derive encryption key + const key = await deriveKey(passphrase, salt); + + // Encrypt the private key + const enc = new TextEncoder(); + const encrypted = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv: iv + }, + key, + enc.encode(armoredKey) + ); + + // Store in IndexedDB + const db = await initDB(); + const transaction = db.transaction([STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + + await store.put({ + id: 'primary_key', + salt: Array.from(salt), + iv: Array.from(iv), + encrypted: Array.from(new Uint8Array(encrypted)), + timestamp: Date.now() + }); + + return true; + } catch (error) { + console.error('Failed to save encrypted key:', error); + throw new Error('Failed to save encrypted key'); + } +} + +// Unlock and retrieve private key from IndexedDB +export async function unlockPrivateKey(passphrase) { + if (typeof window === 'undefined') { + throw new Error('Storage operations can only be performed in the browser'); + } + + if (!passphrase) { + throw new Error('Passphrase required'); + } + + try { + const db = await initDB(); + const transaction = db.transaction([STORE_NAME], 'readonly'); + const store = transaction.objectStore(STORE_NAME); + + const data = await new Promise((resolve, reject) => { + const request = store.get('primary_key'); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + + if (!data) { + throw new Error('No encrypted key found'); + } + + // Reconstruct typed arrays + const salt = new Uint8Array(data.salt); + const iv = new Uint8Array(data.iv); + const encrypted = new Uint8Array(data.encrypted); + + // Derive decryption key + const key = await deriveKey(passphrase, salt); + + // Decrypt + const decrypted = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: iv + }, + key, + encrypted + ); + + const dec = new TextDecoder(); + return dec.decode(decrypted); + } catch (error) { + console.error('Failed to unlock private key:', error); + throw new Error('Failed to unlock private key. Check your passphrase.'); + } +} + +// Check if an encrypted key exists +export async function hasEncryptedKey() { + if (typeof window === 'undefined') return false; + + try { + const db = await initDB(); + const transaction = db.transaction([STORE_NAME], 'readonly'); + const store = transaction.objectStore(STORE_NAME); + + const data = await new Promise((resolve) => { + const request = store.get('primary_key'); + request.onsuccess = () => resolve(request.result); + request.onerror = () => resolve(null); + }); + + return !!data; + } catch { + return false; + } +} + +// Remove encrypted key from IndexedDB +export async function removeEncryptedPrivateKey() { + if (typeof window === 'undefined') return; + + try { + const db = await initDB(); + const transaction = db.transaction([STORE_NAME], 'readwrite'); + const store = transaction.objectStore(STORE_NAME); + await store.delete('primary_key'); + } catch (error) { + console.error('Failed to remove encrypted key:', error); + } +} + +export async function signMessage(message, privateKeyArmored, passphrase) { if (typeof window === 'undefined') { throw new Error('PGP operations can only be performed in the browser'); } + if (!passphrase) { + throw new Error('Passphrase required to unlock private key'); + } + const { decryptKey, readPrivateKey, createMessage, sign } = await import('openpgp'); const privateKey = await decryptKey({ @@ -83,17 +296,15 @@ export async function verifySignature(message, signature, publicKeyArmored) { } } -export function storePrivateKey(privateKey) { - if (typeof window === 'undefined') return; - localStorage.setItem('pgp_private_key', privateKey); +// DEPRECATED - DO NOT USE +export function storePrivateKey() { + throw new Error('Plaintext key storage is disabled for security. Use saveEncryptedPrivateKey() instead.'); } export function getStoredPrivateKey() { - if (typeof window === 'undefined') return null; - return localStorage.getItem('pgp_private_key'); + throw new Error('Plaintext key storage is disabled for security. Use unlockPrivateKey() instead.'); } export function removeStoredPrivateKey() { - if (typeof window === 'undefined') return; - localStorage.removeItem('pgp_private_key'); + throw new Error('Plaintext key storage is disabled for security. Use removeEncryptedPrivateKey() instead.'); } \ No newline at end of file diff --git a/frontend/src/lib/stores/auth.js b/frontend/src/lib/stores/auth.js index 78a3dc2..622edb1 100644 --- a/frontend/src/lib/stores/auth.js +++ b/frontend/src/lib/stores/auth.js @@ -5,7 +5,6 @@ import { goto } from '$app/navigation'; function createAuthStore() { const { subscribe, set, update } = writable({ user: null, - token: null, loading: true }); @@ -15,29 +14,21 @@ function createAuthStore() { async init() { if (!browser) return; - const token = localStorage.getItem('auth_token'); - if (!token) { - set({ user: null, token: null, loading: false }); - return; - } - + // Use cookie-based auth - no localStorage tokens try { const response = await fetch('/api/user/me', { - headers: { - 'Authorization': `Bearer ${token}` - } + credentials: 'include' // Send cookies }); if (response.ok) { const data = await response.json(); - set({ user: data.user, token, loading: false }); + set({ user: data.user, loading: false }); } else { - localStorage.removeItem('auth_token'); - set({ user: null, token: null, loading: false }); + set({ user: null, loading: false }); } } catch (error) { console.error('Auth init error:', error); - set({ user: null, token: null, loading: false }); + set({ user: null, loading: false }); } }, @@ -45,14 +36,15 @@ function createAuthStore() { const response = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', // Receive httpOnly cookie body: JSON.stringify(credentials) }); const data = await response.json(); if (response.ok && data.success) { - localStorage.setItem('auth_token', data.token); - set({ user: data.user, token: data.token, loading: false }); + // Server sets httpOnly cookie, we just store user data + set({ user: data.user, loading: false }); goto('/'); return { success: true }; } @@ -64,14 +56,15 @@ function createAuthStore() { const response = await fetch('/api/auth/pgp-verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', // Receive httpOnly cookie body: JSON.stringify({ username, signature, challenge }) }); const data = await response.json(); if (response.ok && data.success) { - localStorage.setItem('auth_token', data.token); - set({ user: data.user, token: data.token, loading: false }); + // Server sets httpOnly cookie, we just store user data + set({ user: data.user, loading: false }); goto('/'); return { success: true }; } @@ -83,6 +76,7 @@ function createAuthStore() { const response = await fetch('/api/auth/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify(userData) }); @@ -95,47 +89,32 @@ function createAuthStore() { return { success: false, error: data.error || 'Registration failed' }; }, -async updateColor(color) { - const token = localStorage.getItem('auth_token'); - const response = await fetch('/api/user/color', { - method: 'PUT', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ color }) - }); - - const data = await response.json(); - - if (response.ok && data.success) { - // IMPORTANT: Store the new token that includes the updated color - if (data.token) { - localStorage.setItem('auth_token', data.token); + async updateColor(color) { + const response = await fetch('/api/user/color', { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', // Use cookies for auth + body: JSON.stringify({ color }) + }); - // Update the store with new token and user data - update(state => ({ - ...state, - token: data.token, - user: { - ...state.user, - userColor: data.color, - colorCode: data.color // Make sure both fields are updated - } - })); - } else { - // Fallback if no new token (shouldn't happen with current backend) - update(state => ({ - ...state, - user: { ...state.user, userColor: data.color, colorCode: data.color } - })); - } - - return { success: true, color: data.color }; - } - - return { success: false, error: data.error || 'Failed to update color' }; -}, + const data = await response.json(); + + if (response.ok && data.success) { + // Update the store with new user data + update(state => ({ + ...state, + user: { + ...state.user, + userColor: data.color, + colorCode: data.color + } + })); + + return { success: true, color: data.color }; + } + + return { success: false, error: data.error || 'Failed to update color' }; + }, updateUser(userData) { update(state => ({ @@ -144,9 +123,14 @@ async updateColor(color) { })); }, - logout() { - localStorage.removeItem('auth_token'); - set({ user: null, token: null, loading: false }); + async logout() { + // Call logout endpoint to clear httpOnly cookie + await fetch('/api/auth/logout', { + method: 'POST', + credentials: 'include' + }); + + set({ user: null, loading: false }); goto('/login'); } }; diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index 53f8987..5922e96 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -9,6 +9,8 @@ let username = ''; let password = ''; let confirmPassword = ''; + let keyPassphrase = ''; + let confirmKeyPassphrase = ''; let error = ''; let loading = false; let pgpLoading = false; @@ -22,10 +24,14 @@ let showGeneratedKeys = false; let generatedPrivateKey = ''; let generatedPublicKey = ''; + let saveKeyLocally = false; // Show PGP command example let showPgpExample = false; + // For unlocking stored key + let storedKeyPassphrase = ''; + onMount(async () => { await auth.init(); if ($isAuthenticated) { @@ -82,11 +88,22 @@ return; } + if (keyPassphrase !== confirmKeyPassphrase) { + error = 'Key passphrases do not match'; + return; + } + + const passphraseError = pgp.validatePassphrase(keyPassphrase); + if (passphraseError) { + error = passphraseError; + return; + } + loading = true; try { - // Generate PGP key pair - const keyPair = await pgp.generateKeyPair(username); + // Generate PGP key pair with passphrase + const keyPair = await pgp.generateKeyPair(username, keyPassphrase); const result = await auth.register({ username, @@ -96,10 +113,7 @@ }); if (result.success) { - // Store private key locally - pgp.storePrivateKey(keyPair.privateKey); - - // Show keys + // Save keys for display generatedPrivateKey = keyPair.privateKey; generatedPublicKey = keyPair.publicKey; showGeneratedKeys = true; @@ -107,13 +121,38 @@ error = result.error; } } catch (e) { - error = 'Failed to generate PGP keys'; + error = e.message || 'Failed to generate PGP keys'; console.error(e); } loading = false; } + async function proceedAfterKeys() { + loading = true; + + try { + // If user wants to save locally, store encrypted + if (saveKeyLocally) { + await pgp.saveEncryptedPrivateKey(keyPassphrase, generatedPrivateKey); + } + + // Auto-login after registration + const result = await auth.login({ username, password }); + if (result.success) { + goto('/'); + } else { + showGeneratedKeys = false; + mode = 'login'; + error = 'Registration successful. Please login.'; + } + } catch (e) { + error = 'Failed to save keys: ' + e.message; + } + + loading = false; + } + async function initiatePgpLogin() { error = ''; pgpLoading = true; @@ -133,6 +172,13 @@ // Clear the signature field pgpSignature = ''; + + // Check if we have a stored key + const hasKey = await pgp.hasEncryptedKey(); + if (hasKey) { + // We'll need to unlock it + storedKeyPassphrase = ''; + } } else { error = data.error || 'User not found or PGP not enabled'; } @@ -144,6 +190,26 @@ pgpLoading = false; } + async function signWithStoredKey() { + error = ''; + loading = true; + + try { + // Unlock the stored private key + const privateKey = await pgp.unlockPrivateKey(storedKeyPassphrase); + + // Sign the challenge + pgpSignature = await pgp.signMessage(pgpChallenge, privateKey, storedKeyPassphrase); + + // Clear passphrase for security + storedKeyPassphrase = ''; + } catch (e) { + error = 'Failed to unlock key or sign: ' + e.message; + } + + loading = false; + } + async function handlePgpLogin() { error = ''; loading = true; @@ -172,6 +238,7 @@ pgpChallenge = ''; pgpPublicKey = ''; pgpSignature = ''; + storedKeyPassphrase = ''; error = ''; } @@ -189,27 +256,13 @@ a.click(); URL.revokeObjectURL(url); } - - async function proceedAfterKeys() { - // Auto-login after registration - loading = true; - const result = await auth.login({ username, password }); - if (result.success) { - goto('/'); - } else { - showGeneratedKeys = false; - mode = 'login'; - error = 'Registration successful. Please login.'; - } - loading = false; - }
{#if showGeneratedKeys}

Your PGP Keys

- Important: Save your private key securely. You will need it to login with PGP. + Important: Save your private key securely. You will need it and your passphrase to login with PGP.

@@ -231,7 +284,7 @@
- +