diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..a34d412e2 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,18 @@ +{ + "name": "Password Manager Codespace", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/Buffer-7.0/Team 87 - PassVault/password-manager", + "postCreateCommand": "bash '/workspaces/Buffer-7.0/Team 87 - PassVault/.devcontainer/install-mysql-client.sh' || true", + "postStartCommand": "bash '/workspaces/Buffer-7.0/Team 87 - PassVault/.devcontainer/start-app.sh' --background || true", + "forwardPorts": [8080, 3306], + "portsAttributes": { + "8080": { + "label": "Password Manager App", + "onAutoForward": "openBrowserOnce" + }, + "3306": { + "label": "MySQL" + } + } +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..7969555d8 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,31 @@ +version: "3.8" + +services: + app: + image: mcr.microsoft.com/devcontainers/java:1-21-bookworm + volumes: + - ..:/workspaces/Buffer-7.0:cached + working_dir: "/workspaces/Buffer-7.0/Team 87 - PassVault/password-manager" + command: sleep infinity + depends_on: + - mysql + environment: + DB_HOST: mysql + DB_PORT: 3306 + DB_NAME: projectdb + DB_USER: root + DB_PASSWORD: rootpassword + + mysql: + image: mysql:8.0 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: projectdb + ports: + - "3306:3306" + volumes: + - mysql-data:/var/lib/mysql + +volumes: + mysql-data: diff --git a/README.md b/README.md deleted file mode 100644 index 93f088a53..000000000 --- a/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Buffer-7.0 -The themes for Buffer 7.0 are - - -1. Enterprise Systems & Process Optimization -2. GreenTech -3. Cybersecurity and Digital Defense -4. Open Innovation diff --git a/Team 87 - PassVault/.devcontainer/devcontainer.json b/Team 87 - PassVault/.devcontainer/devcontainer.json new file mode 100644 index 000000000..d82d89b47 --- /dev/null +++ b/Team 87 - PassVault/.devcontainer/devcontainer.json @@ -0,0 +1,18 @@ +{ + "name": "Password Manager Codespace", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspaces/Buffer-7.0/Team 87 - PassVault/password-manager", + "postCreateCommand": "bash ../.devcontainer/install-mysql-client.sh || true", + "postStartCommand": "bash ../.devcontainer/start-app.sh --background || true", + "forwardPorts": [8080, 3306], + "portsAttributes": { + "8080": { + "label": "Password Manager App", + "onAutoForward": "openBrowserOnce" + }, + "3306": { + "label": "MySQL" + } + } +} diff --git a/Team 87 - PassVault/.devcontainer/docker-compose.yml b/Team 87 - PassVault/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..f6a71c4d3 --- /dev/null +++ b/Team 87 - PassVault/.devcontainer/docker-compose.yml @@ -0,0 +1,31 @@ +version: "3.8" + +services: + app: + image: mcr.microsoft.com/devcontainers/java:1-21-bookworm + volumes: + - ../..:/workspaces/Buffer-7.0:cached + working_dir: "/workspaces/Buffer-7.0/Team 87 - PassVault/password-manager" + command: sleep infinity + depends_on: + - mysql + environment: + DB_HOST: mysql + DB_PORT: 3306 + DB_NAME: projectdb + DB_USER: root + DB_PASSWORD: rootpassword + + mysql: + image: mysql:8.0 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: rootpassword + MYSQL_DATABASE: projectdb + ports: + - "3306:3306" + volumes: + - mysql-data:/var/lib/mysql + +volumes: + mysql-data: diff --git a/Team 87 - PassVault/.devcontainer/install-mysql-client.sh b/Team 87 - PassVault/.devcontainer/install-mysql-client.sh new file mode 100644 index 000000000..4322c23e1 --- /dev/null +++ b/Team 87 - PassVault/.devcontainer/install-mysql-client.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +if command -v mysql >/dev/null 2>&1 && command -v mysqladmin >/dev/null 2>&1; then + exit 0 +fi + +sudo apt-get update +sudo apt-get install -y mariadb-client + +if [ -x /usr/bin/mariadb ]; then + sudo ln -sf /usr/bin/mariadb /usr/local/bin/mysql +fi diff --git a/Team 87 - PassVault/.devcontainer/start-app.sh b/Team 87 - PassVault/.devcontainer/start-app.sh new file mode 100644 index 000000000..437d513fd --- /dev/null +++ b/Team 87 - PassVault/.devcontainer/start-app.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +APP_DIR="$(cd "${SCRIPT_DIR}/../password-manager" && pwd)" +PID_FILE="/tmp/password-manager.pid" +LOG_FILE="/tmp/password-manager.log" +STAMP_FILE="/tmp/password-manager.sha" + +ensure_mysql_client() { + bash "${SCRIPT_DIR}/install-mysql-client.sh" +} + +wait_for_mysql() { + local host="${DB_HOST:-mysql}" + local port="${DB_PORT:-3306}" + local user="${DB_USER:-root}" + local password="${DB_PASSWORD:-rootpassword}" + local attempt + + for attempt in $(seq 1 30); do + if mysqladmin ping --host="${host}" --port="${port}" --user="${user}" --password="${password}" --silent >/dev/null 2>&1; then + return + fi + + sleep 2 + done + + echo "MySQL did not become ready in time." >&2 + exit 1 +} + +app_is_running() { + if [ -f "${PID_FILE}" ]; then + local pid + pid="$(cat "${PID_FILE}")" + if [ -n "${pid}" ] && kill -0 "${pid}" >/dev/null 2>&1; then + return 0 + fi + rm -f "${PID_FILE}" + fi + + if pgrep -f "java .* Main" >/dev/null 2>&1; then + return 0 + fi + + return 1 +} + +current_app_stamp() { + ( + cd "${APP_DIR}" + find . -maxdepth 2 \( -name "*.java" -o -path "./web/*" -o -name "run.sh" \) -type f -print0 | + sort -z | + xargs -0 sha256sum | + sha256sum | + awk '{print $1}' + ) +} + +stop_app() { + if [ -f "${PID_FILE}" ]; then + local pid + pid="$(cat "${PID_FILE}")" + if [ -n "${pid}" ] && kill -0 "${pid}" >/dev/null 2>&1; then + kill "${pid}" >/dev/null 2>&1 || true + fi + rm -f "${PID_FILE}" + fi + + pkill -f "java .* Main" >/dev/null 2>&1 || true +} + +compile_app() { + ( + cd "${APP_DIR}" + javac -cp ".:lib/mysql-connector-j-9.6.0.jar" *.java + ) +} + +launch_app() { + ( + cd "${APP_DIR}" + nohup java -cp ".:lib/mysql-connector-j-9.6.0.jar" Main >"${LOG_FILE}" 2>&1 & + echo $! > "${PID_FILE}" + ) +} + +wait_for_app() { + local attempt + + for attempt in $(seq 1 30); do + if (echo > /dev/tcp/127.0.0.1/8080) >/dev/null 2>&1; then + return + fi + sleep 1 + done + + echo "Password Manager did not start successfully." >&2 + if [ -f "${LOG_FILE}" ]; then + echo "Last application log lines:" >&2 + tail -n 20 "${LOG_FILE}" >&2 + fi + exit 1 +} + +main() { + ensure_mysql_client + local stamp + stamp="$(current_app_stamp)" + + if [ "${1:-}" = "--prepare-only" ]; then + exit 0 + fi + + if app_is_running; then + if [ -f "${STAMP_FILE}" ] && [ "$(cat "${STAMP_FILE}")" = "${stamp}" ]; then + echo "Password Manager is already running at http://localhost:8080" + exit 0 + fi + + echo "Password Manager code changed; restarting app." + stop_app + fi + + wait_for_mysql + compile_app + launch_app + + if [ "${1:-}" = "--background" ]; then + echo "${stamp}" > "${STAMP_FILE}" + echo "Password Manager startup requested in background. Check /tmp/password-manager.log if needed." + exit 0 + fi + + wait_for_app + echo "${stamp}" > "${STAMP_FILE}" + + echo "Password Manager is running at http://localhost:8080" +} + +main "$@" diff --git a/Team 87 - PassVault/README.md b/Team 87 - PassVault/README.md new file mode 100644 index 000000000..b3ca27a9a --- /dev/null +++ b/Team 87 - PassVault/README.md @@ -0,0 +1,67 @@ +

PassVault

+ +

Smart Security for the Modern Web

+ +Team: ColdBlooded + +

+ +

Project Overview

+ +

+PassVault is a secure password manager web application that allows users to safely store, manage, and analyze passwords. +

+It also includes phishing detection and password strength evaluation to enhance cybersecurity. +

+ +
+ +

Key Features

+ + + +
+ +

DSA Concepts Used

+ + + +
+ +

Tech Stack

+ + + +
+ +

How It Works

+ +
    +
  1. User signs up and logs in
  2. +
  3. Dashboard provides security tools
  4. +
  5. Passwords are stored securely in encrypted form
  6. +
  7. System analyzes passwords and detects threats
  8. +
+ +Video Demo Link + +
+ +
diff --git a/Team 87 - PassVault/password-manager/.gitignore b/Team 87 - PassVault/password-manager/.gitignore new file mode 100644 index 000000000..26cc02f88 --- /dev/null +++ b/Team 87 - PassVault/password-manager/.gitignore @@ -0,0 +1,11 @@ +# Compiled Java files +*.class + +# Old storage files (no longer used) +users.txt +passwords.txt +temp.txt + +# OS files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/Team 87 - PassVault/password-manager/AddPasswordHandler.java b/Team 87 - PassVault/password-manager/AddPasswordHandler.java new file mode 100644 index 000000000..76b9b1685 --- /dev/null +++ b/Team 87 - PassVault/password-manager/AddPasswordHandler.java @@ -0,0 +1,46 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class AddPasswordHandler implements HttpHandler { + + public void handle(HttpExchange exchange) throws IOException { + try { + if (!exchange.getRequestMethod().equalsIgnoreCase("POST")) { + exchange.sendResponseHeaders(405, -1); + return; + } + + String session = SessionManager.extractSessionId( + exchange.getRequestHeaders().getFirst("Cookie") + ); + String email = SessionManager.getUser(session); + String vaultKey = SessionManager.getVaultKey(session); + + if (email == null || vaultKey == null) { + exchange.getResponseHeaders().add("Location", "/"); + exchange.sendResponseHeaders(302, -1); + return; + } + + Map form = RequestUtil.parseFormBody(exchange); + String website = form.getOrDefault("website", ""); + String username = form.getOrDefault("username", ""); + String password = form.getOrDefault("password", ""); + + PasswordManager.savePassword(email, website, username, password, vaultKey); + + exchange.getResponseHeaders().add("Location", "/vault"); + exchange.sendResponseHeaders(302, -1); + } catch (Exception e) { + e.printStackTrace(); + String res = "Save failed"; + exchange.sendResponseHeaders(500, res.length()); + exchange.getResponseBody().write(res.getBytes(StandardCharsets.UTF_8)); + } finally { + exchange.close(); + } + } +} diff --git a/Team 87 - PassVault/password-manager/AuthException.java b/Team 87 - PassVault/password-manager/AuthException.java new file mode 100644 index 000000000..b8b297019 --- /dev/null +++ b/Team 87 - PassVault/password-manager/AuthException.java @@ -0,0 +1,6 @@ +public class AuthException extends Exception { + + public AuthException(String message) { + super(message); + } +} diff --git a/Team 87 - PassVault/password-manager/AuthValidation.java b/Team 87 - PassVault/password-manager/AuthValidation.java new file mode 100644 index 000000000..7425aa2d9 --- /dev/null +++ b/Team 87 - PassVault/password-manager/AuthValidation.java @@ -0,0 +1,36 @@ +import java.util.Locale; + +public class AuthValidation { + private static final int MIN_PASSWORD_LENGTH = 8; + + public static String requireValidEmail(String email) throws AuthException { + String normalized = normalizeEmail(email); + int atIndex = normalized.indexOf('@'); + int dotIndex = normalized.lastIndexOf('.'); + + if ( + atIndex <= 0 || + dotIndex <= atIndex + 1 || + dotIndex >= normalized.length() - 1 || + normalized.contains(" ") + ) { + throw new AuthException("Email must include @ and ."); + } + + return normalized; + } + + public static void requireValidPassword(String password) throws AuthException { + if (password == null || password.length() < MIN_PASSWORD_LENGTH) { + throw new AuthException("Password must be at least 8 characters long."); + } + } + + private static String normalizeEmail(String email) { + if (email == null) { + return ""; + } + + return email.trim().toLowerCase(Locale.ROOT); + } +} diff --git a/Team 87 - PassVault/password-manager/DBConnection.java b/Team 87 - PassVault/password-manager/DBConnection.java new file mode 100644 index 000000000..ef6920cb0 --- /dev/null +++ b/Team 87 - PassVault/password-manager/DBConnection.java @@ -0,0 +1,198 @@ +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +public class DBConnection { + private static final String AUTH_DATA_RESET_VERSION = getEnv("AUTH_DATA_RESET_VERSION", "2026-04-21-auth-reset"); + private static final String[] HOSTS = { + getEnv("DB_HOST", "mysql"), + "127.0.0.1", + "localhost" + }; + private static final String PORT = getEnv("DB_PORT", "3306"); + private static final String DATABASE = getEnv("DB_NAME", "projectdb"); + private static final String USER = getEnv("DB_USER", "root"); + private static final String PASSWORD = getEnv("DB_PASSWORD", "rootpassword"); + private static final int CONNECTION_ATTEMPTS = 15; + private static final long RETRY_DELAY_MS = 2000; + private static final String PARAMETERS = + "?createDatabaseIfNotExist=true&useSSL=false&allowPublicKeyRetrieval=true&connectTimeout=5000&socketTimeout=10000"; + + static { + try { + Class.forName("com.mysql.cj.jdbc.Driver"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("MySQL JDBC driver is missing from the classpath.", e); + } + } + + public static Connection getConnection() throws Exception { + SQLException lastException = null; + + for (int attempt = 1; attempt <= CONNECTION_ATTEMPTS; attempt++) { + for (String host : HOSTS) { + String url = "jdbc:mysql://" + host + ":" + PORT + "/" + DATABASE + PARAMETERS; + try { + DriverManager.setLoginTimeout(5); + return DriverManager.getConnection(url, USER, PASSWORD); + } catch (SQLException e) { + lastException = e; + } + } + + if (attempt < CONNECTION_ATTEMPTS) { + Thread.sleep(RETRY_DELAY_MS); + } + } + + throw lastException != null ? lastException : new SQLException("Unable to connect to MySQL."); + } + + private static String getEnv(String key, String fallback) { + String value = System.getenv(key); + return value == null || value.isBlank() ? fallback : value; + } + + public static void initializeDatabase() throws Exception { + try (Connection conn = getConnection(); + Statement stmt = conn.createStatement()) { + + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS users (" + + "email VARCHAR(255) PRIMARY KEY, " + + "salt VARCHAR(255) NOT NULL, " + + "password_hash VARCHAR(255) NOT NULL, " + + "wrap_salt VARCHAR(255) NULL, " + + "wrapped_vault_key TEXT NULL, " + + "wrap_kdf_algorithm VARCHAR(50) NULL, " + + "wrap_kdf_iterations INT NULL)" + ); + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS app_meta (" + + "meta_key VARCHAR(100) PRIMARY KEY, " + + "meta_value VARCHAR(255) NOT NULL)" + ); + ensureColumn(stmt, "users", "wrap_salt", "VARCHAR(255) NULL"); + ensureColumn(stmt, "users", "wrapped_vault_key", "TEXT NULL"); + ensureColumn(stmt, "users", "wrap_kdf_algorithm", "VARCHAR(50) NULL"); + ensureColumn(stmt, "users", "wrap_kdf_iterations", "INT NULL"); + + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS passwords (" + + "id INT AUTO_INCREMENT PRIMARY KEY, " + + "user_email VARCHAR(255) NOT NULL, " + + "website VARCHAR(255) NOT NULL, " + + "username VARCHAR(255) NOT NULL, " + + "encrypted_password TEXT NOT NULL, " + + "strength VARCHAR(50) NOT NULL, " + + "FOREIGN KEY (user_email) REFERENCES users(email) ON DELETE CASCADE)" + ); + + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS phishing_scans (" + + "id INT AUTO_INCREMENT PRIMARY KEY, " + + "user_email VARCHAR(255) NOT NULL, " + + "url TEXT NOT NULL, " + + "score INT NOT NULL, " + + "verdict VARCHAR(50) NOT NULL, " + + "detail TEXT NOT NULL, " + + "reasons TEXT NOT NULL, " + + "scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (user_email) REFERENCES users(email) ON DELETE CASCADE)" + ); + + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS login_history (" + + "id INT AUTO_INCREMENT PRIMARY KEY, " + + "user_email VARCHAR(255) NOT NULL, " + + "latitude DOUBLE NULL, " + + "longitude DOUBLE NULL, " + + "login_time_utc TIMESTAMP DEFAULT CURRENT_TIMESTAMP, " + + "speed_kmph DOUBLE NULL, " + + "risk_status VARCHAR(50) NOT NULL, " + + "FOREIGN KEY (user_email) REFERENCES users(email) ON DELETE CASCADE)" + ); + + stmt.executeUpdate( + "CREATE TABLE IF NOT EXISTS login_security (" + + "user_email VARCHAR(255) PRIMARY KEY, " + + "failed_attempts INT NOT NULL DEFAULT 0, " + + "suspension_level INT NOT NULL DEFAULT 0, " + + "suspended_until_utc TIMESTAMP NULL, " + + "two_factor_required BOOLEAN NOT NULL DEFAULT FALSE, " + + "two_factor_code VARCHAR(20) NULL, " + + "FOREIGN KEY (user_email) REFERENCES users(email) ON DELETE CASCADE)" + ); + + resetAuthDataIfNeeded(conn); + } + } + + private static void ensureColumn(Statement stmt, String tableName, String columnName, String definition) throws SQLException { + try { + stmt.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + definition); + } catch (SQLException e) { + String message = e.getMessage(); + if (message == null || !message.toLowerCase().contains("duplicate column")) { + throw e; + } + } + } + + private static void resetAuthDataIfNeeded(Connection conn) throws SQLException { + String appliedVersion = readMetaValue(conn, "auth_data_reset_version"); + if (AUTH_DATA_RESET_VERSION.equals(appliedVersion)) { + return; + } + + boolean originalAutoCommit = conn.getAutoCommit(); + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + // One-time cleanup so older accounts do not survive the new auth rules rollout. + stmt.executeUpdate("DELETE FROM phishing_scans"); + stmt.executeUpdate("DELETE FROM passwords"); + stmt.executeUpdate("DELETE FROM users"); + stmt.executeUpdate("ALTER TABLE phishing_scans AUTO_INCREMENT = 1"); + stmt.executeUpdate("ALTER TABLE passwords AUTO_INCREMENT = 1"); + upsertMetaValue(conn, "auth_data_reset_version", AUTH_DATA_RESET_VERSION); + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } finally { + conn.setAutoCommit(originalAutoCommit); + } + } + + private static String readMetaValue(Connection conn, String key) throws SQLException { + String sql = "SELECT meta_value FROM app_meta WHERE meta_key = ?"; + + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, key); + + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return rs.getString("meta_value"); + } + } + } + + return null; + } + + private static void upsertMetaValue(Connection conn, String key, String value) throws SQLException { + String sql = + "INSERT INTO app_meta (meta_key, meta_value) VALUES (?, ?) " + + "ON DUPLICATE KEY UPDATE meta_value = VALUES(meta_value)"; + + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, key); + ps.setString(2, value); + ps.executeUpdate(); + } + } +} diff --git a/Team 87 - PassVault/password-manager/DashboardHandler.java b/Team 87 - PassVault/password-manager/DashboardHandler.java new file mode 100644 index 000000000..7b2609bb8 --- /dev/null +++ b/Team 87 - PassVault/password-manager/DashboardHandler.java @@ -0,0 +1,72 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +public class DashboardHandler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String sessionId = SessionManager.extractSessionId( + exchange.getRequestHeaders().getFirst("Cookie") + ); + + if (sessionId == null) { + exchange.getResponseHeaders().add("Location", "/"); + exchange.sendResponseHeaders(302, -1); + return; + } + + String email = SessionManager.getUser(sessionId); + + if (email == null) { + exchange.getResponseHeaders().add("Location", "/"); + exchange.sendResponseHeaders(302, -1); + return; + } + + List list = PasswordManager.getPasswords(email); + int total = list.size(); + int weak = 0; + + for (PasswordEntry p : list) { + if (p.getStrength().equalsIgnoreCase("Weak")) { + weak++; + } + } + + String html = new String( + Files.readAllBytes(Paths.get("web/dashboard.html")), + StandardCharsets.UTF_8 + ); + + html = html.replace("{{TOTAL}}", String.valueOf(total)); + html = html.replace("{{WEAK}}", String.valueOf(weak)); + + exchange.getResponseHeaders().set("Cache-Control", "no-cache, no-store, must-revalidate"); + exchange.getResponseHeaders().set("Pragma", "no-cache"); + exchange.getResponseHeaders().set("Expires", "0"); + exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8"); + + byte[] response = html.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, response.length); + + try (OutputStream os = exchange.getResponseBody()) { + os.write(response); + } + } catch (Exception e) { + e.printStackTrace(); + + String err = "Internal Server Error"; + exchange.sendResponseHeaders(500, err.length()); + exchange.getResponseBody().write(err.getBytes(StandardCharsets.UTF_8)); + } finally { + exchange.close(); + } + } +} diff --git a/Team 87 - PassVault/password-manager/EditPasswordHandler.java b/Team 87 - PassVault/password-manager/EditPasswordHandler.java new file mode 100644 index 000000000..e45eb7006 --- /dev/null +++ b/Team 87 - PassVault/password-manager/EditPasswordHandler.java @@ -0,0 +1,45 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class EditPasswordHandler implements HttpHandler { + + public void handle(HttpExchange exchange) throws IOException { + try { + if (!exchange.getRequestMethod().equalsIgnoreCase("POST")) { + exchange.sendResponseHeaders(405, -1); + return; + } + + String session = SessionManager.extractSessionId( + exchange.getRequestHeaders().getFirst("Cookie") + ); + String email = SessionManager.getUser(session); + String vaultKey = SessionManager.getVaultKey(session); + + if (email == null || vaultKey == null) { + exchange.getResponseHeaders().add("Location", "/"); + exchange.sendResponseHeaders(302, -1); + return; + } + + Map form = RequestUtil.parseFormBody(exchange); + String website = form.getOrDefault("website", ""); + String password = form.getOrDefault("password", ""); + + PasswordManager.updatePassword(email, website, password, vaultKey); + + exchange.getResponseHeaders().add("Location", "/vault"); + exchange.sendResponseHeaders(302, -1); + } catch (Exception e) { + e.printStackTrace(); + String res = "Update failed"; + exchange.sendResponseHeaders(500, res.length()); + exchange.getResponseBody().write(res.getBytes(StandardCharsets.UTF_8)); + } finally { + exchange.close(); + } + } +} diff --git a/Team 87 - PassVault/password-manager/EncryptionUtil.java b/Team 87 - PassVault/password-manager/EncryptionUtil.java new file mode 100644 index 000000000..319650fd8 --- /dev/null +++ b/Team 87 - PassVault/password-manager/EncryptionUtil.java @@ -0,0 +1,78 @@ +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class EncryptionUtil { + + private static final SecureRandom RANDOM = new SecureRandom(); + private static final int GCM_IV_LENGTH = 12; + private static final int GCM_TAG_LENGTH = 128; + private static final String CURRENT_PREFIX = "v2"; + private static final String WRAPPED_KEY_PREFIX = "wrap1"; + + public static String encrypt(String plainText, String vaultKey) throws Exception { + return encryptGcmPayload(plainText, vaultKey, CURRENT_PREFIX); + } + + public static String generateVaultKey() { + byte[] key = new byte[32]; + RANDOM.nextBytes(key); + return Base64.getEncoder().encodeToString(key); + } + + public static String wrapVaultKey(String vaultKey, String wrappingKey) throws Exception { + return encryptGcmPayload(vaultKey, wrappingKey, WRAPPED_KEY_PREFIX); + } + + public static String unwrapVaultKey(String wrappedVaultKey, String wrappingKey) throws Exception { + return decryptGcmPayload(wrappedVaultKey, wrappingKey, WRAPPED_KEY_PREFIX); + } + + private static String encryptGcmPayload(String plainText, String encodedKey, String prefix) throws Exception { + byte[] keyBytes = Base64.getDecoder().decode(encodedKey); + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES"); + + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + byte[] iv = new byte[GCM_IV_LENGTH]; + RANDOM.nextBytes(iv); + + GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec); + + byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); + return prefix + ":" + + Base64.getUrlEncoder().withoutPadding().encodeToString(iv) + ":" + + Base64.getUrlEncoder().withoutPadding().encodeToString(encrypted); + } + + public static String decrypt(String cipherText, String vaultKey) throws Exception { + return decryptCurrent(cipherText, vaultKey); + } + + private static String decryptCurrent(String cipherText, String vaultKey) throws Exception { + return decryptGcmPayload(cipherText, vaultKey, CURRENT_PREFIX); + } + + private static String decryptGcmPayload(String cipherText, String encodedKey, String expectedPrefix) throws Exception { + String[] parts = cipherText.split(":", 3); + if (parts.length != 3 || !expectedPrefix.equals(parts[0])) { + throw new IllegalArgumentException("Invalid encrypted payload."); + } + + byte[] iv = Base64.getUrlDecoder().decode(parts[1]); + byte[] encrypted = Base64.getUrlDecoder().decode(parts[2]); + byte[] keyBytes = Base64.getDecoder().decode(encodedKey); + + SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES"); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec); + + byte[] decrypted = cipher.doFinal(encrypted); + return new String(decrypted, StandardCharsets.UTF_8); + } + +} diff --git a/Team 87 - PassVault/password-manager/GenerateHandler.java b/Team 87 - PassVault/password-manager/GenerateHandler.java new file mode 100644 index 000000000..9f7ae435d --- /dev/null +++ b/Team 87 - PassVault/password-manager/GenerateHandler.java @@ -0,0 +1,33 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.util.Map; + +public class GenerateHandler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + int len = 12; + boolean sym = false; + + Map query = RequestUtil.parseQuery(exchange.getRequestURI().getQuery()); + if (query.containsKey("len")) len = Integer.parseInt(query.get("len")); + if (query.containsKey("sym")) sym = Boolean.parseBoolean(query.get("sym")); + + String pass = PasswordGenerator.generate(len, sym, false); + + exchange.getResponseHeaders().set("Content-Type", "text/plain"); + exchange.sendResponseHeaders(200, pass.length()); + exchange.getResponseBody().write(pass.getBytes()); + } catch (Exception e) { + e.printStackTrace(); + + String err = "Generator error"; + exchange.sendResponseHeaders(500, err.length()); + exchange.getResponseBody().write(err.getBytes()); + } finally { + exchange.close(); + } + } +} diff --git a/Team 87 - PassVault/password-manager/HashUtil.java b/Team 87 - PassVault/password-manager/HashUtil.java new file mode 100644 index 000000000..05570a7e5 --- /dev/null +++ b/Team 87 - PassVault/password-manager/HashUtil.java @@ -0,0 +1,79 @@ +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Base64; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +public class HashUtil { + + private static final String PBKDF2_PREFIX = "PBKDF2_SHA256"; + private static final int DEFAULT_ITERATIONS = 210_000; + private static final int HASH_LENGTH_BITS = 256; + private static final int VAULT_KEY_LENGTH_BITS = 256; + + public static String generateSalt() { + byte[] salt = new byte[16]; + new SecureRandom().nextBytes(salt); + return Base64.getEncoder().encodeToString(salt); + } + + public static String hashPassword(String password, String salt) { + byte[] derived = deriveBytes(password, salt, "auth", DEFAULT_ITERATIONS, HASH_LENGTH_BITS); + return PBKDF2_PREFIX + "$" + DEFAULT_ITERATIONS + "$" + + Base64.getEncoder().encodeToString(derived); + } + + public static boolean verifyPassword(String password, String salt, String storedHash) { + if (storedHash == null || storedHash.isEmpty() || !isPbkdf2Hash(storedHash)) { + return false; + } + + String[] parts = storedHash.split("\\$", 3); + if (parts.length != 3) { + return false; + } + + int iterations = Integer.parseInt(parts[1]); + byte[] expected = Base64.getDecoder().decode(parts[2]); + byte[] actual = deriveBytes(password, salt, "auth", iterations, expected.length * 8); + return MessageDigest.isEqual(expected, actual); + } + + public static boolean isPbkdf2Hash(String storedHash) { + return storedHash != null && storedHash.startsWith(PBKDF2_PREFIX + "$"); + } + + public static String getPasswordKdfAlgorithm() { + return PBKDF2_PREFIX; + } + + public static int getDefaultIterations() { + return DEFAULT_ITERATIONS; + } + + public static String deriveWrappingKey(String password, String salt) { + byte[] key = deriveBytes(password, salt, "wrap", DEFAULT_ITERATIONS, VAULT_KEY_LENGTH_BITS); + return Base64.getEncoder().encodeToString(key); + } + + private static byte[] deriveBytes(String password, String salt, String purpose, int iterations, int lengthBits) { + char[] passwordChars = password.toCharArray(); + + try { + byte[] saltBytes = Base64.getDecoder().decode(salt); + byte[] purposeBytes = purpose.getBytes(StandardCharsets.UTF_8); + byte[] scopedSalt = Arrays.copyOf(saltBytes, saltBytes.length + purposeBytes.length); + System.arraycopy(purposeBytes, 0, scopedSalt, saltBytes.length, purposeBytes.length); + + PBEKeySpec spec = new PBEKeySpec(passwordChars, scopedSalt, iterations, lengthBits); + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + return factory.generateSecret(spec).getEncoded(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + Arrays.fill(passwordChars, '\0'); + } + } +} diff --git a/Team 87 - PassVault/password-manager/LoginHandler.java b/Team 87 - PassVault/password-manager/LoginHandler.java new file mode 100644 index 000000000..cb8857e3a --- /dev/null +++ b/Team 87 - PassVault/password-manager/LoginHandler.java @@ -0,0 +1,110 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Map; + +public class LoginHandler implements HttpHandler { + + public void handle(HttpExchange exchange) throws IOException { + try { + if (!exchange.getRequestMethod().equalsIgnoreCase("POST")) { + exchange.sendResponseHeaders(405, -1); + return; + } + + Map form = RequestUtil.parseFormBody(exchange); + String email = form.getOrDefault("email", ""); + String password = form.getOrDefault("password", ""); + Double latitude = parseNullableDouble(form.get("latitude")); + Double longitude = parseNullableDouble(form.get("longitude")); + + LoginSecurityManager.SuspensionStatus suspension = LoginSecurityManager.getSuspensionStatus(email); + if (suspension.isSuspended()) { + sendText(exchange, 423, pausedAccountMessage(suspension.getSuspendedUntilUtc())); + return; + } + + PasswordManager.LoginResult login = PasswordManager.login(email, password); + if (login != null) { + LoginRiskResult riskResult = LoginSecurityManager.analyzeLogin( + login.getEmail(), + latitude, + longitude + ); + + if (riskResult.isHighRisk()) { + PendingLogin pendingLogin = LoginSecurityManager.createPendingLogin( + login.getEmail(), + login.getVaultKey(), + latitude, + longitude, + riskResult + ); + + exchange.getResponseHeaders().add("Location", "/verify2fa?token=" + pendingLogin.getToken()); + exchange.sendResponseHeaders(302, -1); + return; + } + + LoginSecurityManager.recordSuccessfulLogin(login.getEmail(), latitude, longitude, riskResult); + String session = SessionManager.createSession( + login.getEmail(), + login.getVaultKey() + ); + + exchange.getResponseHeaders().add( + "Set-Cookie", + "session=" + session + "; Path=/; HttpOnly; SameSite=Lax" + ); + exchange.getResponseHeaders().add("Location", "/dashboard"); + + exchange.sendResponseHeaders(302, -1); + return; + } + + LoginSecurityManager.recordFailedLogin(email); + WebUtils.redirectWithFlash(exchange, "/", "login", "error", "Incorrect email or password."); + } catch (AuthException e) { + WebUtils.redirectWithFlash(exchange, "/", "login", "error", e.getMessage()); + } catch (Exception e) { + e.printStackTrace(); + WebUtils.redirectWithFlash(exchange, "/", "login", "error", "Login failed. Please try again."); + } finally { + exchange.close(); + } + } + + private static Double parseNullableDouble(String value) { + if (value == null || value.isBlank()) { + return null; + } + + try { + return Double.parseDouble(value.trim()); + } catch (NumberFormatException e) { + return null; + } + } + + private static void sendText(HttpExchange exchange, int statusCode, String text) throws IOException { + byte[] data = text.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8"); + exchange.sendResponseHeaders(statusCode, data.length); + exchange.getResponseBody().write(data); + } + + private static String pausedAccountMessage(Instant suspendedUntilUtc) { + ZonedDateTime utcTime = suspendedUntilUtc.atZone(ZoneOffset.UTC); + ZonedDateTime indiaTime = suspendedUntilUtc.atZone(java.time.ZoneId.of("Asia/Kolkata")); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd hh:mm:ss a z"); + + return "Account paused after repeated failed logins.\n" + + "Unlock time (India): " + indiaTime.format(formatter) + "\n" + + "Unlock time (UTC): " + utcTime.format(formatter); + } +} diff --git a/Team 87 - PassVault/password-manager/LoginRecord.java b/Team 87 - PassVault/password-manager/LoginRecord.java new file mode 100644 index 000000000..2f66c1628 --- /dev/null +++ b/Team 87 - PassVault/password-manager/LoginRecord.java @@ -0,0 +1,35 @@ +import java.time.Instant; + +public class LoginRecord { + private final String email; + private final Double latitude; + private final Double longitude; + private final Instant loginTimeUtc; + + public LoginRecord(String email, Double latitude, Double longitude, Instant loginTimeUtc) { + this.email = email; + this.latitude = latitude; + this.longitude = longitude; + this.loginTimeUtc = loginTimeUtc; + } + + public String getEmail() { + return email; + } + + public Double getLatitude() { + return latitude; + } + + public Double getLongitude() { + return longitude; + } + + public Instant getLoginTimeUtc() { + return loginTimeUtc; + } + + public boolean hasLocation() { + return latitude != null && longitude != null; + } +} diff --git a/Team 87 - PassVault/password-manager/LoginRiskResult.java b/Team 87 - PassVault/password-manager/LoginRiskResult.java new file mode 100644 index 000000000..04d1d1235 --- /dev/null +++ b/Team 87 - PassVault/password-manager/LoginRiskResult.java @@ -0,0 +1,23 @@ +public class LoginRiskResult { + private final boolean highRisk; + private final String riskStatus; + private final Double speedKmph; + + public LoginRiskResult(boolean highRisk, String riskStatus, Double speedKmph) { + this.highRisk = highRisk; + this.riskStatus = riskStatus; + this.speedKmph = speedKmph; + } + + public boolean isHighRisk() { + return highRisk; + } + + public String getRiskStatus() { + return riskStatus; + } + + public Double getSpeedKmph() { + return speedKmph; + } +} diff --git a/Team 87 - PassVault/password-manager/LoginSecurityManager.java b/Team 87 - PassVault/password-manager/LoginSecurityManager.java new file mode 100644 index 000000000..aed285168 --- /dev/null +++ b/Team 87 - PassVault/password-manager/LoginSecurityManager.java @@ -0,0 +1,302 @@ +import java.security.SecureRandom; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.time.Duration; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Stack; +import java.util.UUID; + +public class LoginSecurityManager { + private static final double IMPOSSIBLE_TRAVEL_LIMIT_KMPH = 1100.0; + private static final int FAILED_ATTEMPTS_PER_SUSPENSION = 5; + private static final int BASE_SUSPENSION_MINUTES = 10; + private static final SecureRandom RANDOM = new SecureRandom(); + + public static class SuspensionStatus { + private final boolean suspended; + private final Instant suspendedUntilUtc; + + public SuspensionStatus(boolean suspended, Instant suspendedUntilUtc) { + this.suspended = suspended; + this.suspendedUntilUtc = suspendedUntilUtc; + } + + public boolean isSuspended() { + return suspended; + } + + public Instant getSuspendedUntilUtc() { + return suspendedUntilUtc; + } + } + + public static SuspensionStatus getSuspensionStatus(String email) throws Exception { + ensureSecurityRow(email); + + String sql = "SELECT suspended_until_utc FROM login_security WHERE user_email = ?"; + try (Connection conn = DBConnection.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, email); + + try (ResultSet rs = stmt.executeQuery()) { + if (!rs.next()) { + return new SuspensionStatus(false, null); + } + + Timestamp suspendedUntil = rs.getTimestamp("suspended_until_utc"); + if (suspendedUntil == null) { + return new SuspensionStatus(false, null); + } + + Instant until = suspendedUntil.toInstant(); + return new SuspensionStatus(until.isAfter(Instant.now()), until); + } + } + } + + public static void recordFailedLogin(String email) throws Exception { + ensureSecurityRow(email); + + String selectSql = "SELECT failed_attempts, suspension_level FROM login_security WHERE user_email = ?"; + try (Connection conn = DBConnection.getConnection(); + PreparedStatement select = conn.prepareStatement(selectSql)) { + select.setString(1, email); + + int failedAttempts = 0; + int suspensionLevel = 0; + try (ResultSet rs = select.executeQuery()) { + if (rs.next()) { + failedAttempts = rs.getInt("failed_attempts"); + suspensionLevel = rs.getInt("suspension_level"); + } + } + + failedAttempts++; + Instant suspendedUntil = null; + int nextSuspensionLevel = suspensionLevel; + + if (failedAttempts % FAILED_ATTEMPTS_PER_SUSPENSION == 0) { + int minutes = BASE_SUSPENSION_MINUTES * (int) Math.pow(2, suspensionLevel); + suspendedUntil = Instant.now().plus(Duration.ofMinutes(minutes)); + nextSuspensionLevel = suspensionLevel + 1; + } + + String updateSql = + "UPDATE login_security SET failed_attempts = ?, suspension_level = ?, " + + "suspended_until_utc = COALESCE(?, suspended_until_utc) WHERE user_email = ?"; + try (PreparedStatement update = conn.prepareStatement(updateSql)) { + update.setInt(1, failedAttempts); + update.setInt(2, nextSuspensionLevel); + update.setTimestamp(3, suspendedUntil == null ? null : Timestamp.from(suspendedUntil)); + update.setString(4, email); + update.executeUpdate(); + } + } + } + + public static void resetFailedAttempts(String email) throws Exception { + ensureSecurityRow(email); + + String sql = + "UPDATE login_security SET failed_attempts = 0, suspended_until_utc = NULL " + + "WHERE user_email = ?"; + try (Connection conn = DBConnection.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, email); + stmt.executeUpdate(); + } + } + + public static LoginRiskResult analyzeLogin(String email, Double latitude, Double longitude) throws Exception { + Stack historyStack = getLoginHistoryStack(email); + Instant currentTimeUtc = Instant.now(); + + Map currentLogin = new HashMap<>(); + currentLogin.put("latitude", latitude); + currentLogin.put("longitude", longitude); + currentLogin.put("timeUtc", currentTimeUtc); + + if (historyStack.isEmpty()) { + return new LoginRiskResult(false, "FIRST_LOGIN", null); + } + + LoginRecord previousLogin = historyStack.peek(); + Double currentLatitude = (Double) currentLogin.get("latitude"); + Double currentLongitude = (Double) currentLogin.get("longitude"); + Instant currentInstant = (Instant) currentLogin.get("timeUtc"); + + if (!previousLogin.hasLocation() || currentLatitude == null || currentLongitude == null) { + return new LoginRiskResult(false, "LOCATION_UNAVAILABLE", null); + } + + double distanceKm = haversineKm( + previousLogin.getLatitude(), + previousLogin.getLongitude(), + currentLatitude, + currentLongitude + ); + long seconds = Math.max( + Duration.between(previousLogin.getLoginTimeUtc(), currentInstant).getSeconds(), + 1 + ); + double speedKmph = distanceKm / (seconds / 3600.0); + + if (speedKmph > IMPOSSIBLE_TRAVEL_LIMIT_KMPH) { + return new LoginRiskResult(true, "HIGH_RISK_IMPOSSIBLE_TRAVEL", speedKmph); + } + + return new LoginRiskResult(false, "NORMAL", speedKmph); + } + + public static void recordSuccessfulLogin( + String email, + Double latitude, + Double longitude, + LoginRiskResult riskResult + ) throws Exception { + resetFailedAttempts(email); + + String sql = + "INSERT INTO login_history " + + "(user_email, latitude, longitude, login_time_utc, speed_kmph, risk_status) " + + "VALUES (?, ?, ?, UTC_TIMESTAMP(), ?, ?)"; + try (Connection conn = DBConnection.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, email); + setNullableDouble(stmt, 2, latitude); + setNullableDouble(stmt, 3, longitude); + setNullableDouble(stmt, 4, riskResult.getSpeedKmph()); + stmt.setString(5, riskResult.getRiskStatus()); + stmt.executeUpdate(); + } + + clearTwoFactor(email); + } + + public static PendingLogin createPendingLogin( + String email, + String vaultKey, + Double latitude, + Double longitude, + LoginRiskResult riskResult + ) throws Exception { + String token = UUID.randomUUID().toString(); + String code = String.format("%06d", RANDOM.nextInt(1_000_000)); + PendingLogin pendingLogin = new PendingLogin(token, email, vaultKey, latitude, longitude, code, riskResult); + PendingLoginStore.put(pendingLogin); + + String sql = + "UPDATE login_security SET two_factor_required = TRUE, two_factor_code = ? " + + "WHERE user_email = ?"; + ensureSecurityRow(email); + try (Connection conn = DBConnection.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, code); + stmt.setString(2, email); + stmt.executeUpdate(); + } + + return pendingLogin; + } + + public static PendingLogin completePendingLogin(String token, String code) throws Exception { + PendingLogin pendingLogin = PendingLoginStore.get(token); + if (pendingLogin == null || code == null || !pendingLogin.getCode().equals(code.trim())) { + return null; + } + + PendingLoginStore.remove(token); + recordSuccessfulLogin( + pendingLogin.getEmail(), + pendingLogin.getLatitude(), + pendingLogin.getLongitude(), + pendingLogin.getRiskResult() + ); + return pendingLogin; + } + + private static Stack getLoginHistoryStack(String email) throws Exception { + Stack stack = new Stack<>(); + + String sql = + "SELECT user_email, latitude, longitude, login_time_utc FROM login_history " + + "WHERE user_email = ? ORDER BY login_time_utc ASC, id ASC"; + try (Connection conn = DBConnection.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, email); + + try (ResultSet rs = stmt.executeQuery()) { + while (rs.next()) { + stack.push(new LoginRecord( + rs.getString("user_email"), + getNullableDouble(rs, "latitude"), + getNullableDouble(rs, "longitude"), + rs.getTimestamp("login_time_utc").toInstant() + )); + } + } + } + + return stack; + } + + private static void ensureSecurityRow(String email) throws Exception { + if (email == null || email.isBlank()) { + return; + } + + String sql = + "INSERT INTO login_security (user_email) " + + "SELECT ? FROM users WHERE email = ? " + + "ON DUPLICATE KEY UPDATE user_email = user_email"; + try (Connection conn = DBConnection.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, email); + stmt.setString(2, email); + stmt.executeUpdate(); + } + } + + private static void clearTwoFactor(String email) throws Exception { + String sql = + "UPDATE login_security SET two_factor_required = FALSE, two_factor_code = NULL " + + "WHERE user_email = ?"; + try (Connection conn = DBConnection.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, email); + stmt.executeUpdate(); + } + } + + private static double haversineKm(double lat1, double lon1, double lat2, double lon2) { + final double earthRadiusKm = 6371.0; + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + double a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(Math.toRadians(lat1)) * + Math.cos(Math.toRadians(lat2)) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return earthRadiusKm * c; + } + + private static void setNullableDouble(PreparedStatement stmt, int index, Double value) throws Exception { + if (value == null) { + stmt.setNull(index, java.sql.Types.DOUBLE); + return; + } + + stmt.setDouble(index, value); + } + + private static Double getNullableDouble(ResultSet rs, String column) throws Exception { + double value = rs.getDouble(column); + return rs.wasNull() ? null : value; + } +} diff --git a/Team 87 - PassVault/password-manager/LogoutHandler.java b/Team 87 - PassVault/password-manager/LogoutHandler.java new file mode 100644 index 000000000..ed2deb7cb --- /dev/null +++ b/Team 87 - PassVault/password-manager/LogoutHandler.java @@ -0,0 +1,26 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; + +public class LogoutHandler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) { + try { + String session = SessionManager.extractSessionId( + exchange.getRequestHeaders().getFirst("Cookie") + ); + SessionManager.removeSession(session); + + exchange.getResponseHeaders().add( + "Set-Cookie", + "session=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax" + ); + + exchange.getResponseHeaders().add("Location", "/logged-out.html"); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/Team 87 - PassVault/password-manager/Main.java b/Team 87 - PassVault/password-manager/Main.java new file mode 100644 index 000000000..850ab7466 --- /dev/null +++ b/Team 87 - PassVault/password-manager/Main.java @@ -0,0 +1,116 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class Main { + private static final int DATABASE_INIT_ATTEMPTS = 30; + private static final long DATABASE_INIT_DELAY_MS = 2000; + + public static void main(String[] args) throws Exception { + HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0); + + server.createContext("/", Main::serveStaticFile); + server.createContext("/register", new RegisterHandler()); + server.createContext("/login", new LoginHandler()); + server.createContext("/verify2fa", new TwoFactorPageHandler()); + server.createContext("/submit2fa", new TwoFactorVerifyHandler()); + server.createContext("/addPassword", new AddPasswordHandler()); + server.createContext("/dashboard", new DashboardHandler()); + server.createContext("/vault", new VaultHandler()); + server.createContext("/phishing", new PhishingPageHandler()); + server.createContext("/analyzePhishing", new PhishingAnalyzeHandler()); + server.createContext("/reveal", new RevealHandler()); + server.createContext("/logout", new LogoutHandler()); + server.createContext("/editPassword", new EditPasswordHandler()); + server.createContext("/generate", new GenerateHandler()); + + server.start(); + System.out.println("Running on http://localhost:8080"); + startDatabaseInitializer(); + } + + private static void startDatabaseInitializer() { + Thread dbInitializer = new Thread(() -> { + for (int attempt = 1; attempt <= DATABASE_INIT_ATTEMPTS; attempt++) { + try { + DBConnection.initializeDatabase(); + System.out.println("Database initialized"); + return; + } catch (Exception e) { + System.err.println( + "Database initialization attempt " + attempt + " failed: " + e.getMessage() + ); + + if (attempt == DATABASE_INIT_ATTEMPTS) { + e.printStackTrace(); + return; + } + + try { + Thread.sleep(DATABASE_INIT_DELAY_MS); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + return; + } + } + } + }); + + dbInitializer.setDaemon(true); + dbInitializer.start(); + } + + private static void serveStaticFile(HttpExchange exchange) throws IOException { + try { + String path = exchange.getRequestURI().getPath(); + Path webRoot = Paths.get("web").toAbsolutePath().normalize(); + + if (path.equals("/")) { + path = "/index.html"; + } + + Path requested = webRoot.resolve(path.substring(1)).normalize(); + + if (!requested.startsWith(webRoot) || Files.isDirectory(requested) || !Files.exists(requested)) { + sendText(exchange, 404, "404 Not Found", "text/plain"); + return; + } + + byte[] data = Files.readAllBytes(requested); + exchange.getResponseHeaders().set("Content-Type", contentTypeFor(path)); + exchange.sendResponseHeaders(200, data.length); + exchange.getResponseBody().write(data); + } catch (Exception e) { + e.printStackTrace(); + sendText(exchange, 500, "Internal Server Error", "text/plain"); + } finally { + exchange.close(); + } + } + + private static void sendText(HttpExchange exchange, int status, String body, String contentType) throws IOException { + byte[] data = body.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", contentType + "; charset=UTF-8"); + exchange.sendResponseHeaders(status, data.length); + exchange.getResponseBody().write(data); + } + + private static String contentTypeFor(String path) { + if (path.endsWith(".html")) { + return "text/html; charset=UTF-8"; + } + if (path.endsWith(".css")) { + return "text/css; charset=UTF-8"; + } + if (path.endsWith(".js")) { + return "application/javascript; charset=UTF-8"; + } + + return "application/octet-stream"; + } +} diff --git a/Team 87 - PassVault/password-manager/PasswordEntry.java b/Team 87 - PassVault/password-manager/PasswordEntry.java new file mode 100644 index 000000000..44355da5d --- /dev/null +++ b/Team 87 - PassVault/password-manager/PasswordEntry.java @@ -0,0 +1,41 @@ +public class PasswordEntry { + + private String website; + private String username; + private String encryptedPassword; + private String strength; + + public PasswordEntry(String website, String username, String encryptedPassword, String strength) { + this.website = website; + this.username = username; + this.encryptedPassword = encryptedPassword; + this.strength = strength; + } + + public String getWebsite() { + return website; + } + + public String getUsername() { + return username; + } + + public String getEncryptedPassword() { + return encryptedPassword; + } + + public String getStrength() { + return strength; + } + + // Convert object → file format + public String toFileString() { + return website + "," + username + "," + encryptedPassword + "," + strength; + } + + // Convert file → object + public static PasswordEntry fromFileString(String line) { + String[] parts = line.split(","); + return new PasswordEntry(parts[0], parts[1], parts[2], parts[3]); + } +} \ No newline at end of file diff --git a/Team 87 - PassVault/password-manager/PasswordGenerator.java b/Team 87 - PassVault/password-manager/PasswordGenerator.java new file mode 100644 index 000000000..6e6794cbc --- /dev/null +++ b/Team 87 - PassVault/password-manager/PasswordGenerator.java @@ -0,0 +1,120 @@ +import java.security.SecureRandom; +import java.util.*; + +public class PasswordGenerator { + + private static final String LOWER = "abcdefghijklmnopqrstuvwxyz"; + private static final String UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String DIGITS = "0123456789"; + private static final String SYMBOLS = "!@#$%^&*-_."; + + private static final SecureRandom rand = new SecureRandom(); + + public static String generate(int length, boolean useSymbols, boolean avoidAmbiguous) { + + // 🔒 enforce range + if (length < 6) length = 6; + if (length > 18) length = 18; + + int maxAttempts = 40; + + String best = ""; + String bestStrength = "Weak"; + + for (int i = 0; i < maxAttempts; i++) { + + String pwd = generateOnce(length, useSymbols, avoidAmbiguous); + + if (hasBadPatterns(pwd)) continue; + + String strength = PasswordStrength.getStrength(pwd); + + // 🔥 return immediately if strong + if (strength.equals("Strong")) { + return pwd; + } + + // store best seen + if (strength.equals("Medium") && bestStrength.equals("Weak")) { + best = pwd; + bestStrength = "Medium"; + } + + if (best.isEmpty()) { + best = pwd; + } + } + + return best; + } + + private static String generateOnce(int length, boolean useSymbols, boolean avoidAmbiguous) { + + List password = new ArrayList<>(); + + String lower = LOWER; + String upper = UPPER; + String digits = DIGITS; + String symbols = SYMBOLS; + + if (avoidAmbiguous) { + lower = lower.replaceAll("[l]", ""); + upper = upper.replaceAll("[IO]", ""); + digits = digits.replaceAll("[01]", ""); + } + + password.add(randomChar(lower)); + password.add(randomChar(upper)); + password.add(randomChar(digits)); + if (useSymbols) password.add(randomChar(symbols)); + + String full = lower + upper + digits; + if (useSymbols) full += symbols; + + while (password.size() < length) { + password.add(randomChar(full)); + } + + shuffle(password); + + StringBuilder sb = new StringBuilder(); + for (char c : password) sb.append(c); + + return sb.toString(); + } + + private static char randomChar(String s) { + return s.charAt(rand.nextInt(s.length())); + } + + private static void shuffle(List list) { + for (int i = list.size() - 1; i > 0; i--) { + int j = rand.nextInt(i + 1); + char t = list.get(i); + list.set(i, list.get(j)); + list.set(j, t); + } + } + + private static boolean hasBadPatterns(String s) { + + for (int i = 0; i < s.length() - 2; i++) { + if (s.charAt(i+1) == s.charAt(i) + 1 && + s.charAt(i+2) == s.charAt(i+1) + 1) return true; + } + + for (int i = 0; i < s.length() - 2; i++) { + if (s.charAt(i) == s.charAt(i+1) && + s.charAt(i) == s.charAt(i+2)) return true; + } + + String lower = s.toLowerCase(); + String[] patterns = {"qwerty", "asdf", "zxcv", "12345"}; + + for (String p : patterns) { + if (lower.contains(p)) return true; + } + + return false; + } +} \ No newline at end of file diff --git a/Team 87 - PassVault/password-manager/PasswordManager.java b/Team 87 - PassVault/password-manager/PasswordManager.java new file mode 100644 index 000000000..0e90e7523 --- /dev/null +++ b/Team 87 - PassVault/password-manager/PasswordManager.java @@ -0,0 +1,175 @@ +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLIntegrityConstraintViolationException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class PasswordManager { + + public static final class LoginResult { + private final String email; + private final String vaultKey; + + public LoginResult(String email, String vaultKey) { + this.email = email; + this.vaultKey = vaultKey; + } + + public String getEmail() { + return email; + } + + public String getVaultKey() { + return vaultKey; + } + } + + public static void register(String email, String password) throws Exception { + String normalizedEmail = AuthValidation.requireValidEmail(email); + AuthValidation.requireValidPassword(password); + String salt = HashUtil.generateSalt(); + String hash = HashUtil.hashPassword(password, salt); + String wrapSalt = HashUtil.generateSalt(); + String wrappingKey = HashUtil.deriveWrappingKey(password, wrapSalt); + String vaultKey = EncryptionUtil.generateVaultKey(); + String wrappedVaultKey = EncryptionUtil.wrapVaultKey(vaultKey, wrappingKey); + String sql = + "INSERT INTO users (email, salt, password_hash, wrap_salt, wrapped_vault_key, wrap_kdf_algorithm, wrap_kdf_iterations) " + + "VALUES (?, ?, ?, ?, ?, ?, ?)"; + + try (Connection conn = DBConnection.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, normalizedEmail); + ps.setString(2, salt); + ps.setString(3, hash); + ps.setString(4, wrapSalt); + ps.setString(5, wrappedVaultKey); + ps.setString(6, HashUtil.getPasswordKdfAlgorithm()); + ps.setInt(7, HashUtil.getDefaultIterations()); + ps.executeUpdate(); + } catch (SQLIntegrityConstraintViolationException e) { + throw new AuthException("An account with this email already exists."); + } + } + + public static LoginResult login(String email, String password) throws Exception { + String normalizedEmail = AuthValidation.requireValidEmail(email); + AuthValidation.requireValidPassword(password); + String sql = + "SELECT salt, password_hash, wrap_salt, wrapped_vault_key, wrap_kdf_algorithm, wrap_kdf_iterations " + + "FROM users WHERE email = ?"; + + try (Connection conn = DBConnection.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, normalizedEmail); + + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) { + return null; + } + + String salt = rs.getString("salt"); + String storedHash = rs.getString("password_hash"); + String wrapSalt = rs.getString("wrap_salt"); + String wrappedVaultKey = rs.getString("wrapped_vault_key"); + + if (!HashUtil.verifyPassword(password, salt, storedHash)) { + return null; + } + + if (isWrappedVaultKeyMissing(wrapSalt, wrappedVaultKey)) { + return null; + } + + String wrappingKey = HashUtil.deriveWrappingKey(password, wrapSalt); + String vaultKey = EncryptionUtil.unwrapVaultKey(wrappedVaultKey, wrappingKey); + + return new LoginResult(normalizedEmail, vaultKey); + } + } + } + + public static void savePassword(String email, String website, String username, String password, String vaultKey) throws Exception { + String encrypted = EncryptionUtil.encrypt(password, vaultKey); + String strength = PasswordStrength.getStrength(password); + + String sql = "INSERT INTO passwords (user_email, website, username, encrypted_password, strength) VALUES (?, ?, ?, ?, ?)"; + + try (Connection conn = DBConnection.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, email); + ps.setString(2, website); + ps.setString(3, username); + ps.setString(4, encrypted); + ps.setString(5, strength); + ps.executeUpdate(); + } + } + + public static List getPasswords(String email) throws Exception { + List list = new ArrayList<>(); + String sql = "SELECT website, username, encrypted_password, strength FROM passwords WHERE user_email = ?"; + + try (Connection conn = DBConnection.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, email); + + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + PasswordEntry entry = new PasswordEntry( + rs.getString("website"), + rs.getString("username"), + rs.getString("encrypted_password"), + rs.getString("strength") + ); + list.add(entry); + } + } + } + + return list; + } + + public static int countReusedPasswords(List list) { + Set seen = new HashSet<>(); + int reused = 0; + + for (PasswordEntry p : list) { + String enc = p.getEncryptedPassword(); + + if (!seen.add(enc)) { + reused++; + } + } + + return reused; + } + + public static void updatePassword(String email, String website, String newPassword, String vaultKey) throws Exception { + String encrypted = EncryptionUtil.encrypt(newPassword, vaultKey); + String strength = PasswordStrength.getStrength(newPassword); + + String sql = "UPDATE passwords SET encrypted_password = ?, strength = ? WHERE user_email = ? AND website = ?"; + + try (Connection conn = DBConnection.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, encrypted); + ps.setString(2, strength); + ps.setString(3, email); + ps.setString(4, website); + ps.executeUpdate(); + } + } + + private static boolean isWrappedVaultKeyMissing(String wrapSalt, String wrappedVaultKey) { + return wrapSalt == null || wrapSalt.isBlank() || wrappedVaultKey == null || wrappedVaultKey.isBlank(); + } +} diff --git a/Team 87 - PassVault/password-manager/PasswordStrength.java b/Team 87 - PassVault/password-manager/PasswordStrength.java new file mode 100644 index 000000000..1dda23fe6 --- /dev/null +++ b/Team 87 - PassVault/password-manager/PasswordStrength.java @@ -0,0 +1,131 @@ +import java.util.*; + +public class PasswordStrength { + + private static final Set dictionary = new HashSet<>(Arrays.asList( + "password", "admin", "qwerty", "abc", "letmein", "welcome", "login" + )); + + public static String getStrength(String password) { + int score = scorePassword(password); + + if (score >= 8) return "Strong"; + if (score >= 5) return "Medium"; + return "Weak"; + } + + public static long estimateGuesses(String password) { + int n = password.length(); + long[] dp = new long[n + 1]; + + Arrays.fill(dp, Long.MAX_VALUE); + dp[0] = 1; + + for (int i = 0; i < n; i++) { + if (dp[i] == Long.MAX_VALUE) continue; + + for (int j = i + 1; j <= n; j++) { + String sub = password.substring(i, j); + long cost = getPatternCost(sub); + + if (cost > 0 && dp[i] <= Long.MAX_VALUE / cost) { + dp[j] = Math.min(dp[j], dp[i] * cost); + } else { + dp[j] = Math.min(dp[j], Long.MAX_VALUE); + } + } + } + + return dp[n]; + } + + private static int scorePassword(String password) { + int score = 0; + int length = password.length(); + + if (length >= 12) score += 4; + else if (length >= 10) score += 3; + else if (length >= 8) score += 2; + else if (length >= 6) score += 1; + + int variety = 0; + if (password.matches(".*[a-z].*")) variety++; + if (password.matches(".*[A-Z].*")) variety++; + if (password.matches(".*[0-9].*")) variety++; + if (password.matches(".*[^a-zA-Z0-9].*")) variety++; + score += variety * 2; + + if (dictionary.contains(password.toLowerCase())) score -= 4; + if (isSequence(password)) score -= 3; + if (isRepeat(password)) score -= 3; + if (isKeyboardPattern(password)) score -= 3; + + long guesses = estimateGuesses(password); + if (guesses >= 1_000_000L) score += 1; + if (guesses >= 100_000_000L) score += 1; + + return Math.max(score, 0); + } + + private static long getPatternCost(String s) { + if (dictionary.contains(s.toLowerCase())) { + return 100_000L; + } + + if (isSequence(s)) { + return 1_000L; + } + + if (isRepeat(s)) { + return 500L; + } + + if (isKeyboardPattern(s)) { + return 2_000L; + } + + int charset = 0; + + if (s.matches(".*[a-z].*")) charset += 26; + if (s.matches(".*[A-Z].*")) charset += 26; + if (s.matches(".*[0-9].*")) charset += 10; + if (s.matches(".*[^a-zA-Z0-9].*")) charset += 20; + + if (charset == 0) charset = 10; + + double entropy = s.length() * (Math.log(charset) / Math.log(2)); + return Math.max((long) Math.pow(2, entropy / 2), 1L); + } + + private static boolean isSequence(String s) { + if (s.length() < 3) return false; + + for (int i = 0; i < s.length() - 2; i++) { + if (s.charAt(i + 1) == s.charAt(i) + 1 && + s.charAt(i + 2) == s.charAt(i + 1) + 1) { + return true; + } + } + return false; + } + + private static boolean isRepeat(String s) { + if (s.length() < 3) return false; + + char first = s.charAt(0); + for (char c : s.toCharArray()) { + if (c != first) return false; + } + return true; + } + + private static boolean isKeyboardPattern(String s) { + String[] patterns = {"qwerty", "asdf", "zxcv", "12345"}; + + String lower = s.toLowerCase(); + for (String p : patterns) { + if (lower.contains(p)) return true; + } + return false; + } +} diff --git a/Team 87 - PassVault/password-manager/PendingLogin.java b/Team 87 - PassVault/password-manager/PendingLogin.java new file mode 100644 index 000000000..e4e677e64 --- /dev/null +++ b/Team 87 - PassVault/password-manager/PendingLogin.java @@ -0,0 +1,55 @@ +public class PendingLogin { + private final String token; + private final String email; + private final String vaultKey; + private final Double latitude; + private final Double longitude; + private final String code; + private final LoginRiskResult riskResult; + + public PendingLogin( + String token, + String email, + String vaultKey, + Double latitude, + Double longitude, + String code, + LoginRiskResult riskResult + ) { + this.token = token; + this.email = email; + this.vaultKey = vaultKey; + this.latitude = latitude; + this.longitude = longitude; + this.code = code; + this.riskResult = riskResult; + } + + public String getToken() { + return token; + } + + public String getEmail() { + return email; + } + + public String getVaultKey() { + return vaultKey; + } + + public Double getLatitude() { + return latitude; + } + + public Double getLongitude() { + return longitude; + } + + public String getCode() { + return code; + } + + public LoginRiskResult getRiskResult() { + return riskResult; + } +} diff --git a/Team 87 - PassVault/password-manager/PendingLoginStore.java b/Team 87 - PassVault/password-manager/PendingLoginStore.java new file mode 100644 index 000000000..53ab79ee1 --- /dev/null +++ b/Team 87 - PassVault/password-manager/PendingLoginStore.java @@ -0,0 +1,26 @@ +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class PendingLoginStore { + private static final Map pendingLogins = new ConcurrentHashMap<>(); + + public static void put(PendingLogin pendingLogin) { + pendingLogins.put(pendingLogin.getToken(), pendingLogin); + } + + public static PendingLogin get(String token) { + if (token == null || token.isBlank()) { + return null; + } + + return pendingLogins.get(token); + } + + public static PendingLogin remove(String token) { + if (token == null || token.isBlank()) { + return null; + } + + return pendingLogins.remove(token); + } +} diff --git a/Team 87 - PassVault/password-manager/PhishingAnalysisResult.java b/Team 87 - PassVault/password-manager/PhishingAnalysisResult.java new file mode 100644 index 000000000..ffb9e6219 --- /dev/null +++ b/Team 87 - PassVault/password-manager/PhishingAnalysisResult.java @@ -0,0 +1,58 @@ +import java.util.LinkedList; +import java.util.List; + +public class PhishingAnalysisResult { + private final String url; + private final int score; + private final String verdict; + private final String detail; + private final LinkedList reasons; + private final String protocol; + private final String domain; + private final String subdomain; + private final String rootDomain; + private final String tld; + private final String path; + private final boolean plainHttpTrustedDomain; + + public PhishingAnalysisResult( + String url, + int score, + String verdict, + String detail, + LinkedList reasons, + String protocol, + String domain, + String subdomain, + String rootDomain, + String tld, + String path, + boolean plainHttpTrustedDomain + ) { + this.url = url; + this.score = score; + this.verdict = verdict; + this.detail = detail; + this.reasons = new LinkedList<>(reasons); + this.protocol = protocol; + this.domain = domain; + this.subdomain = subdomain; + this.rootDomain = rootDomain; + this.tld = tld; + this.path = path; + this.plainHttpTrustedDomain = plainHttpTrustedDomain; + } + + public String getUrl() { return url; } + public int getScore() { return score; } + public String getVerdict() { return verdict; } + public String getDetail() { return detail; } + public List getReasons() { return reasons; } + public String getProtocol() { return protocol; } + public String getDomain() { return domain; } + public String getSubdomain() { return subdomain; } + public String getRootDomain() { return rootDomain; } + public String getTld() { return tld; } + public String getPath() { return path; } + public boolean isPlainHttpTrustedDomain() { return plainHttpTrustedDomain; } +} diff --git a/Team 87 - PassVault/password-manager/PhishingAnalyzeHandler.java b/Team 87 - PassVault/password-manager/PhishingAnalyzeHandler.java new file mode 100644 index 000000000..4c40524b0 --- /dev/null +++ b/Team 87 - PassVault/password-manager/PhishingAnalyzeHandler.java @@ -0,0 +1,69 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class PhishingAnalyzeHandler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + if (!exchange.getRequestMethod().equalsIgnoreCase("POST")) { + WebUtils.sendJson(exchange, 405, "{\"error\":\"Method not allowed\"}"); + return; + } + + String sessionId = WebUtils.getSessionId(exchange); + String email = SessionManager.getUser(sessionId); + if (email == null) { + WebUtils.sendJson(exchange, 401, "{\"error\":\"Unauthorized\"}"); + return; + } + + String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + Map form = WebUtils.parseFormBody(body); + String url = form.getOrDefault("url", "").trim(); + + if (url.isEmpty()) { + WebUtils.sendJson(exchange, 400, "{\"error\":\"URL is required\"}"); + return; + } + + PhishingAnalysisResult result = PhishingService.analyzeAndStore(email, url); + WebUtils.sendJson(exchange, 200, buildJson(result)); + } catch (Exception e) { + e.printStackTrace(); + WebUtils.sendJson(exchange, 500, "{\"error\":\"Unable to analyze URL\"}"); + } + } + + private String buildJson(PhishingAnalysisResult result) { + StringBuilder reasons = new StringBuilder("["); + for (int i = 0; i < result.getReasons().size(); i++) { + reasons.append("\"") + .append(WebUtils.jsonEscape(result.getReasons().get(i))) + .append("\""); + if (i < result.getReasons().size() - 1) { + reasons.append(","); + } + } + reasons.append("]"); + + return "{" + + "\"url\":\"" + WebUtils.jsonEscape(result.getUrl()) + "\"," + + "\"score\":" + result.getScore() + "," + + "\"verdict\":\"" + WebUtils.jsonEscape(result.getVerdict()) + "\"," + + "\"detail\":\"" + WebUtils.jsonEscape(result.getDetail()) + "\"," + + "\"reasons\":" + reasons + "," + + "\"components\":{" + + "\"protocol\":\"" + WebUtils.jsonEscape(result.getProtocol()) + "\"," + + "\"domain\":\"" + WebUtils.jsonEscape(result.getDomain()) + "\"," + + "\"subdomain\":\"" + WebUtils.jsonEscape(result.getSubdomain()) + "\"," + + "\"rootDomain\":\"" + WebUtils.jsonEscape(result.getRootDomain()) + "\"," + + "\"tld\":\"" + WebUtils.jsonEscape(result.getTld()) + "\"," + + "\"path\":\"" + WebUtils.jsonEscape(result.getPath()) + "\"" + + "}" + + "}"; + } +} diff --git a/Team 87 - PassVault/password-manager/PhishingDetectorEngine.java b/Team 87 - PassVault/password-manager/PhishingDetectorEngine.java new file mode 100644 index 000000000..d746efc15 --- /dev/null +++ b/Team 87 - PassVault/password-manager/PhishingDetectorEngine.java @@ -0,0 +1,594 @@ +import java.net.URI; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class PhishingDetectorEngine { + + private static final Pattern FORM_ACTION_PATTERN = + Pattern.compile("action\\s*=\\s*[\"']?([^\"'\\s>]+)", Pattern.CASE_INSENSITIVE); + private static final Pattern HIDDEN_INPUT_PATTERN = + Pattern.compile("]*type\\s*=\\s*[\"']hidden[\"'][^>]*>", Pattern.CASE_INSENSITIVE); + + private final PhishingTrie trustedDomainTrie; + private final HashMap homoglyphMap; + private final HashMap lookalikeMap; + private final HashMap scoreWeights; + private final HashMap shortenerRiskTiers; + private final LinkedList trustedDomains; + private final LinkedList reasons; + private boolean trustedDomainMatch; + private boolean trustedFinalDomainMatch; + private boolean criticalThreat; + private boolean strongSuspiciousSignal; + private boolean shortenerDetected; + private int totalScore; + + public PhishingDetectorEngine() { + trustedDomainTrie = new PhishingTrie(); + homoglyphMap = new HashMap<>(); + lookalikeMap = new HashMap<>(); + scoreWeights = new HashMap<>(); + shortenerRiskTiers = new HashMap<>(); + trustedDomains = new LinkedList<>(); + reasons = new LinkedList<>(); + initTrustedDomains(); + initHomoglyphMap(); + initLookalikeMap(); + initScoreWeights(); + initShortenerRiskTiers(); + } + + public PhishingAnalysisResult analyze(String inputUrl) { + PhishingURLParser parser = new PhishingURLParser(); + parser.parse(inputUrl); + reasons.clear(); + totalScore = 0; + trustedDomainMatch = false; + trustedFinalDomainMatch = false; + criticalThreat = false; + strongSuspiciousSignal = false; + shortenerDetected = false; + + checkTrustedDomain(parser); + checkHTTPS(parser); + checkLookalikeDomain(parser); + checkBrandMisuse(parser); + checkHomoglyph(parser); + checkPunycode(parser); + checkShortener(parser); + checkTLD(parser); + checkSubdomain(parser); + checkHyphen(parser); + checkKeywords(parser); + checkIPAddress(parser); + inspectLivePage(inputUrl, parser); + normalizeTrustedDomainVerdict(parser); + + int finalScore = criticalThreat ? Math.max(totalScore, 11) : totalScore; + boolean plainHttpTrustedDomain = trustedDomainMatch && "http".equals(parser.protocol); + + return new PhishingAnalysisResult( + inputUrl, + finalScore, + PhishingScoreEngine.getVerdict(finalScore, plainHttpTrustedDomain), + PhishingScoreEngine.getVerdictDetail(finalScore, plainHttpTrustedDomain), + reasons, + parser.protocol, + parser.domain, + parser.subdomain, + parser.rootDomain, + parser.tld, + parser.path, + plainHttpTrustedDomain + ); + } + + private void initTrustedDomains() { + String[] domains = { + "sbi.co.in", "hdfcbank.com", "icicibank.com", "axisbank.com", "kotakbank.com", + "pnbindia.in", "bankofbaroda.in", "canarabank.com", "unionbankofindia.co.in", + "indusind.com", "yesbank.in", "paytm.com", "phonepe.com", "razorpay.com", + "mobikwik.com", "paypal.com", "wise.com", "stripe.com", "mastercard.com", + "visa.com", "google.com", "gmail.com", "youtube.com", "microsoft.com", + "apple.com", "github.com", "stackoverflow.com", "linkedin.com", "zoom.us", + "dropbox.com", "slack.com", "notion.so", "atlassian.com", "amazon.com", + "amazon.in", "flipkart.com", "myntra.com", "meesho.com", "snapdeal.com", + "ebay.com", "shopify.com", "facebook.com", "instagram.com", "twitter.com", + "whatsapp.com", "reddit.com", "pinterest.com", "telegram.org", "netflix.com", + "hotstar.com", "spotify.com", "primevideo.com", "jiocinema.com", "wikipedia.org", + "khanacademy.org", "coursera.org", "udemy.com", "nptel.ac.in", "gov.in", + "uidai.gov.in", "irctc.co.in", "incometax.gov.in", "mca.gov.in", + "digitalindia.gov.in", "digilocker.gov.in" + }; + + for (String domain : domains) { + trustedDomainTrie.insert(domain); + trustedDomains.add(domain); + } + } + + private void initHomoglyphMap() { + homoglyphMap.put('\u0430', 'a'); + homoglyphMap.put('\u0435', 'e'); + homoglyphMap.put('\u043E', 'o'); + homoglyphMap.put('\u0440', 'p'); + homoglyphMap.put('\u0441', 'c'); + homoglyphMap.put('\u0456', 'i'); + homoglyphMap.put('\u0455', 's'); + homoglyphMap.put('\u0501', 'd'); + homoglyphMap.put('\u03BF', 'o'); + homoglyphMap.put('\u03F2', 'c'); + homoglyphMap.put('\u03C5', 'u'); + homoglyphMap.put('\u217C', 'l'); + homoglyphMap.put('\u0261', 'g'); + homoglyphMap.put('\u0131', 'i'); + homoglyphMap.put('\u0185', 'b'); + homoglyphMap.put('\u0292', 'z'); + } + + private void initLookalikeMap() { + lookalikeMap.put('0', 'o'); + lookalikeMap.put('1', 'l'); + lookalikeMap.put('3', 'e'); + lookalikeMap.put('4', 'a'); + lookalikeMap.put('5', 's'); + lookalikeMap.put('7', 't'); + lookalikeMap.put('8', 'b'); + lookalikeMap.put('@', 'a'); + lookalikeMap.put('$', 's'); + } + + private void initScoreWeights() { + scoreWeights.put("TrustedDomain", -6); + scoreWeights.put("HTTPS", -2); + scoreWeights.put("LookalikeDomain", 7); + scoreWeights.put("BrandMisuse", 5); + scoreWeights.put("Homoglyph", 6); + scoreWeights.put("Punycode", 3); + scoreWeights.put("Shortener", 2); + scoreWeights.put("RiskyShortener", 4); + scoreWeights.put("SuspiciousTLD", 3); + scoreWeights.put("SubdomainAbuse", 3); + scoreWeights.put("HyphenOveruse", 2); + scoreWeights.put("KeywordFound", 3); + scoreWeights.put("IPAddress", 5); + scoreWeights.put("RedirectDepth", 3); + scoreWeights.put("RedirectLoop", 5); + scoreWeights.put("RedirectDomainChange", 4); + scoreWeights.put("RedirectDowngrade", 4); + scoreWeights.put("PasswordForm", 6); + scoreWeights.put("ExternalFormAction", 5); + scoreWeights.put("BrandContentMismatch", 5); + scoreWeights.put("HiddenFieldAbuse", 2); + scoreWeights.put("SuspiciousScript", 3); + } + + private void initShortenerRiskTiers() { + String[] establishedShorteners = { + "bit.ly", "tinyurl.com", "t.co", "lnkd.in", "goo.gl", "ow.ly", + "is.gd", "buff.ly", "rebrand.ly", "cutt.ly", "short.io", "bl.ink" + }; + String[] elevatedRiskShorteners = { + "goo.su" + }; + + for (String domain : establishedShorteners) { + shortenerRiskTiers.put(domain, "ESTABLISHED"); + } + for (String domain : elevatedRiskShorteners) { + shortenerRiskTiers.put(domain, "ELEVATED_RISK"); + } + } + + private void addReason(String ruleName, String message) { + int points = scoreWeights.getOrDefault(ruleName, 0); + totalScore += points; + String prefix = points >= 0 ? "+" + points : String.valueOf(points); + reasons.add(prefix + " " + message); + } + + private void checkTrustedDomain(PhishingURLParser url) { + if (isTrustedDomain(url)) { + trustedDomainMatch = true; + addReason("TrustedDomain", "Domain is in the trusted-domain trie: " + url.domain); + } + } + + private void checkHTTPS(PhishingURLParser url) { + if ("https".equals(url.protocol)) { + addReason("HTTPS", "HTTPS protocol detected for encrypted transport."); + } + } + + private void checkLookalikeDomain(PhishingURLParser url) { + String normalizedDomain = normalizeLookalikes(url.domain); + if (!url.domain.equals(normalizedDomain) && isTrustedDomain(normalizedDomain)) { + criticalThreat = true; + strongSuspiciousSignal = true; + addReason("LookalikeDomain", "Domain visually imitates trusted site " + normalizedDomain + "."); + return; + } + + for (String trustedDomain : trustedDomains) { + if (url.domain.equals(trustedDomain)) { + continue; + } + + if (!sameFamily(url, trustedDomain)) { + continue; + } + + int distance = levenshteinDistance(url.domain, trustedDomain); + if (distance > 0 && distance <= 2) { + criticalThreat = true; + strongSuspiciousSignal = true; + addReason("LookalikeDomain", "Domain is only " + distance + " edit away from trusted site " + trustedDomain + "."); + return; + } + } + } + + private void checkBrandMisuse(PhishingURLParser url) { + String[] brands = getKnownBrands(); + + String normalizedDomain = normalizeLookalikes(url.domain); + for (String brand : brands) { + if ((url.domain.contains(brand) || normalizedDomain.contains(brand)) && !isTrustedDomain(url)) { + addReason("BrandMisuse", "Known brand '" + brand + "' appears inside a non-official domain."); + return; + } + } + } + + private void checkHomoglyph(PhishingURLParser url) { + StringBuilder normalized = new StringBuilder(); + for (char ch : url.domain.toCharArray()) { + normalized.append(homoglyphMap.getOrDefault(ch, ch)); + } + + String normalizedDomain = normalized.toString(); + if (!url.domain.equals(normalizedDomain) && isTrustedDomain(normalizedDomain)) { + criticalThreat = true; + strongSuspiciousSignal = true; + addReason("Homoglyph", "Unicode lookalike characters suggest impersonation of " + normalizedDomain + "."); + } + } + + private void checkPunycode(PhishingURLParser url) { + if (url.domain.startsWith("xn--") || url.domain.contains(".xn--")) { + addReason("Punycode", "Punycode encoding detected, which is common in IDN spoofing."); + } + } + + private void checkShortener(PhishingURLParser url) { + String shortenerTier = shortenerRiskTiers.get(url.domain); + if (shortenerTier == null) { + return; + } + + shortenerDetected = true; + addReason("Shortener", "Shortened URL detected, so the final destination needs verification."); + + if ("ELEVATED_RISK".equals(shortenerTier)) { + strongSuspiciousSignal = true; + addReason("RiskyShortener", "This shortener has elevated abuse reports, so treat it with extra caution."); + } else { + reasons.add("+0 Established shortener detected, so the redirect target matters more than the homepage."); + } + } + + private void checkTLD(PhishingURLParser url) { + String[] suspiciousTlds = { + ".xyz", ".top", ".click", ".biz", ".tk", ".ml", ".ga", ".cf", + ".gq", ".pw", ".rest", ".zip", ".mov", ".fit", ".surf" + }; + + for (String tld : suspiciousTlds) { + if (url.tld.equals(tld)) { + addReason("SuspiciousTLD", "High-risk TLD detected: " + tld); + return; + } + } + } + + private void checkSubdomain(PhishingURLParser url) { + int dotCount = 0; + for (char ch : url.domain.toCharArray()) { + if (ch == '.') { + dotCount++; + } + } + if (dotCount > 2) { + addReason("SubdomainAbuse", "Excessive subdomains detected (" + dotCount + " dots)."); + } + } + + private void checkHyphen(PhishingURLParser url) { + int hyphenCount = 0; + for (char ch : url.domain.toCharArray()) { + if (ch == '-') { + hyphenCount++; + } + } + if (hyphenCount >= 2) { + addReason("HyphenOveruse", "Multiple hyphens in the domain resemble common phishing naming patterns."); + } + } + + private void checkKeywords(PhishingURLParser url) { + String[] keywords = { + "login", "verify", "update", "secure", "password", "bank", "account", + "signin", "confirm", "alert", "suspend", "urgent", "validate", "recover", + "unlock", "billing", "checkout", "cardverify", "otp", "kyc" + }; + + String value = url.rawInput.toLowerCase(); + for (String keyword : keywords) { + if (value.contains(keyword)) { + addReason("KeywordFound", "Suspicious keyword found in the URL: '" + keyword + "'."); + return; + } + } + } + + private void checkIPAddress(PhishingURLParser url) { + if (url.domain.matches("\\d{1,3}(\\.\\d{1,3}){3}")) { + addReason("IPAddress", "Raw IP address used instead of a normal domain name."); + } + } + + private void inspectLivePage(String inputUrl, PhishingURLParser originalParser) { + PhishingFetchResult fetchResult = PhishingPageFetcher.fetch(inputUrl); + if (!fetchResult.wasFetched()) { + return; + } + + analyzeRedirects(fetchResult, originalParser); + + String html = fetchResult.getHtml(); + PhishingURLParser finalParser = new PhishingURLParser(); + finalParser.parse(fetchResult.getFinalUrl()); + reconcileShortenerRisk(fetchResult, finalParser); + + if (html == null || html.isBlank()) { + return; + } + + analyzeFetchedHtml(html, finalParser); + } + + private void reconcileShortenerRisk(PhishingFetchResult fetchResult, PhishingURLParser finalParser) { + if (!shortenerDetected) { + return; + } + + boolean trustedFinalDomain = isTrustedDomain(finalParser); + boolean httpsFinalDomain = "https".equals(finalParser.protocol); + boolean resolvedThroughRedirect = fetchResult.getRedirectChain().size() > 1; + + if (trustedFinalDomain && httpsFinalDomain && resolvedThroughRedirect && !fetchResult.hasRedirectLoop()) { + totalScore -= scoreWeights.getOrDefault("Shortener", 0); + reasons.add("-2 Shortened link resolves cleanly to a trusted HTTPS destination."); + } + } + + private void analyzeRedirects(PhishingFetchResult fetchResult, PhishingURLParser originalParser) { + LinkedList chain = fetchResult.getRedirectChain(); + if (fetchResult.hasRedirectLoop()) { + criticalThreat = true; + strongSuspiciousSignal = true; + addReason("RedirectLoop", "Redirect loop detected while expanding the URL."); + } + + if (fetchResult.isRedirectLimitReached() || chain.size() > 3) { + addReason("RedirectDepth", "Long redirect chain detected before reaching the final page."); + } + + for (int i = 1; i < chain.size(); i++) { + PhishingURLParser previous = new PhishingURLParser(); + PhishingURLParser current = new PhishingURLParser(); + previous.parse(chain.get(i - 1)); + current.parse(chain.get(i)); + + if ("https".equals(previous.protocol) && "http".equals(current.protocol)) { + strongSuspiciousSignal = true; + addReason("RedirectDowngrade", "Redirect chain downgrades from HTTPS to HTTP."); + break; + } + } + + if (chain.size() > 1) { + PhishingURLParser finalParser = new PhishingURLParser(); + finalParser.parse(fetchResult.getFinalUrl()); + if (!sameFamily(finalParser, originalParser.domain)) { + strongSuspiciousSignal = true; + addReason("RedirectDomainChange", "Redirect chain ends on a different domain: " + finalParser.domain + "."); + } + } + } + + private void analyzeFetchedHtml(String html, PhishingURLParser finalParser) { + String lowerHtml = html.toLowerCase(); + boolean trustedFinalDomain = isTrustedDomain(finalParser); + trustedFinalDomainMatch = trustedFinalDomain; + boolean hasPasswordField = containsPasswordField(lowerHtml); + boolean hasForm = lowerHtml.contains("= 4) { + addReason("HiddenFieldAbuse", "Page contains many hidden form fields, which is a common phishing trait."); + } + + if (!trustedFinalDomain && containsSuspiciousScript(lowerHtml)) { + addReason("SuspiciousScript", "Page contains obfuscated or redirect-heavy script patterns."); + } + + String normalizedDomain = normalizeLookalikes(finalParser.domain); + for (String brand : getKnownBrands()) { + if (lowerHtml.contains(brand) && !trustedFinalDomain && !normalizedDomain.contains(brand)) { + addReason("BrandContentMismatch", "Page content mentions trusted brand '" + brand + "' on an unrelated domain."); + strongSuspiciousSignal = true; + if (hasPasswordField) { + criticalThreat = true; + } + return; + } + } + } + + private boolean containsPasswordField(String lowerHtml) { + return lowerHtml.contains("type=\"password\"") + || lowerHtml.contains("type='password'") + || lowerHtml.contains("type=password"); + } + + private boolean hasSuspiciousFormAction(String html, PhishingURLParser currentUrl) { + Matcher matcher = FORM_ACTION_PATTERN.matcher(html); + while (matcher.find()) { + String action = matcher.group(1).trim(); + if (action.isEmpty() || action.startsWith("/") || action.startsWith("#")) { + continue; + } + + try { + URI actionUri = URI.create(action); + String actionHost = actionUri.getHost(); + if (actionHost == null) { + continue; + } + + PhishingURLParser actionParser = new PhishingURLParser(); + actionParser.parse(action); + boolean sameHost = actionHost.equalsIgnoreCase(currentUrl.domain); + boolean sameTrustedFamily = isTrustedSameBoundary(currentUrl, actionParser); + + if (!sameHost && !sameTrustedFamily) { + return true; + } + } catch (Exception ignored) { + // Non-absolute action targets are not treated as suspicious here. + } + } + return false; + } + + private void normalizeTrustedDomainVerdict(PhishingURLParser originalParser) { + boolean plainHttpTrustedDomain = trustedDomainMatch && "http".equals(originalParser.protocol); + boolean trustedBoundary = trustedDomainMatch || trustedFinalDomainMatch; + + if (trustedBoundary && !strongSuspiciousSignal && !plainHttpTrustedDomain) { + totalScore = Math.min(totalScore, 0); + reasons.add("-trusted Trusted domain baseline suppresses weak generic page heuristics."); + } + } + + private int countHiddenInputs(String html) { + Matcher matcher = HIDDEN_INPUT_PATTERN.matcher(html); + int count = 0; + while (matcher.find()) { + count++; + } + return count; + } + + private boolean containsSuspiciousScript(String lowerHtml) { + return lowerHtml.contains("eval(") + || lowerHtml.contains("atob(") + || lowerHtml.contains("fromcharcode(") + || lowerHtml.contains("document.location") + || lowerHtml.contains("window.location"); + } + + private String[] getKnownBrands() { + return new String[] { + "paypal", "google", "amazon", "apple", "microsoft", "facebook", "netflix", + "linkedin", "twitter", "sbi", "hdfc", "icici", "axis", "kotak", "flipkart", + "instagram", "whatsapp", "spotify", "youtube", "gmail", "paytm", "phonepe", + "razorpay", "irctc", "uidai" + }; + } + + private String normalizeLookalikes(String domain) { + StringBuilder normalized = new StringBuilder(); + for (char ch : domain.toCharArray()) { + char unicodeNormalized = homoglyphMap.getOrDefault(ch, ch); + normalized.append(lookalikeMap.getOrDefault(unicodeNormalized, unicodeNormalized)); + } + return normalized.toString(); + } + + private boolean sameRegisteredDomain(PhishingURLParser left, PhishingURLParser right) { + return left.rootDomain.equals(right.rootDomain) && left.tld.equals(right.tld); + } + + private boolean isTrustedSameBoundary(PhishingURLParser currentUrl, PhishingURLParser actionUrl) { + return isTrustedDomain(currentUrl) + && sameRegisteredDomain(currentUrl, actionUrl); + } + + private boolean isTrustedDomain(PhishingURLParser url) { + return url != null && isTrustedDomain(url.domain); + } + + private boolean isTrustedDomain(String domain) { + if (domain == null || domain.isBlank()) { + return false; + } + + if (trustedDomainTrie.search(domain)) { + return true; + } + + int firstDot = domain.indexOf('.'); + if (firstDot == -1) { + return false; + } + + return trustedDomainTrie.search(domain.substring(firstDot + 1)); + } + + private boolean sameFamily(PhishingURLParser url, String trustedDomain) { + String trustedTld = extractTld(trustedDomain); + return url.tld.equals(trustedTld) || url.domain.endsWith(trustedTld) || trustedDomain.endsWith(url.tld); + } + + private String extractTld(String domain) { + int lastDot = domain.lastIndexOf('.'); + return lastDot == -1 ? "" : domain.substring(lastDot); + } + + private int levenshteinDistance(String left, String right) { + int[][] dp = new int[left.length() + 1][right.length() + 1]; + + for (int i = 0; i <= left.length(); i++) { + dp[i][0] = i; + } + for (int j = 0; j <= right.length(); j++) { + dp[0][j] = j; + } + + for (int i = 1; i <= left.length(); i++) { + for (int j = 1; j <= right.length(); j++) { + int cost = left.charAt(i - 1) == right.charAt(j - 1) ? 0 : 1; + dp[i][j] = Math.min( + Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1), + dp[i - 1][j - 1] + cost + ); + } + } + + return dp[left.length()][right.length()]; + } +} diff --git a/Team 87 - PassVault/password-manager/PhishingFetchResult.java b/Team 87 - PassVault/password-manager/PhishingFetchResult.java new file mode 100644 index 000000000..c1120cd0e --- /dev/null +++ b/Team 87 - PassVault/password-manager/PhishingFetchResult.java @@ -0,0 +1,41 @@ +import java.util.LinkedList; + +public class PhishingFetchResult { + private final String startUrl; + private final String finalUrl; + private final String contentType; + private final String html; + private final LinkedList redirectChain; + private final boolean fetched; + private final boolean redirectLoop; + private final boolean redirectLimitReached; + + public PhishingFetchResult( + String startUrl, + String finalUrl, + String contentType, + String html, + LinkedList redirectChain, + boolean fetched, + boolean redirectLoop, + boolean redirectLimitReached + ) { + this.startUrl = startUrl; + this.finalUrl = finalUrl; + this.contentType = contentType; + this.html = html; + this.redirectChain = new LinkedList<>(redirectChain); + this.fetched = fetched; + this.redirectLoop = redirectLoop; + this.redirectLimitReached = redirectLimitReached; + } + + public String getStartUrl() { return startUrl; } + public String getFinalUrl() { return finalUrl; } + public String getContentType() { return contentType; } + public String getHtml() { return html; } + public LinkedList getRedirectChain() { return new LinkedList<>(redirectChain); } + public boolean wasFetched() { return fetched; } + public boolean hasRedirectLoop() { return redirectLoop; } + public boolean isRedirectLimitReached() { return redirectLimitReached; } +} diff --git a/Team 87 - PassVault/password-manager/PhishingPageFetcher.java b/Team 87 - PassVault/password-manager/PhishingPageFetcher.java new file mode 100644 index 000000000..cadef401c --- /dev/null +++ b/Team 87 - PassVault/password-manager/PhishingPageFetcher.java @@ -0,0 +1,126 @@ +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.LinkedList; + +public class PhishingPageFetcher { + private static final int CONNECT_TIMEOUT_MS = 5000; + private static final int READ_TIMEOUT_MS = 7000; + private static final int MAX_REDIRECTS = 5; + private static final int MAX_HTML_BYTES = 200000; + + public static PhishingFetchResult fetch(String inputUrl) { + String currentUrl = ensureScheme(inputUrl); + LinkedList redirectChain = new LinkedList<>(); + HashSet visited = new HashSet<>(); + redirectChain.add(currentUrl); + visited.add(currentUrl); + + boolean redirectLoop = false; + boolean redirectLimitReached = false; + + for (int hop = 0; hop <= MAX_REDIRECTS; hop++) { + HttpURLConnection connection = null; + try { + URI currentUri = URI.create(currentUrl); + URL url = currentUri.toURL(); + connection = (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); + connection.setConnectTimeout(CONNECT_TIMEOUT_MS); + connection.setReadTimeout(READ_TIMEOUT_MS); + connection.setRequestProperty("User-Agent", "PassVault-PhishingDetector/1.0"); + connection.setRequestProperty("Accept", "text/html,application/xhtml+xml,*/*"); + connection.setRequestMethod("GET"); + + int status = connection.getResponseCode(); + if (isRedirect(status)) { + String location = connection.getHeaderField("Location"); + if (location == null || location.isBlank()) { + break; + } + + String resolvedUrl = currentUri.resolve(location).toString(); + if (visited.contains(resolvedUrl)) { + redirectLoop = true; + redirectChain.add(resolvedUrl); + break; + } + + redirectChain.add(resolvedUrl); + visited.add(resolvedUrl); + currentUrl = resolvedUrl; + + if (hop == MAX_REDIRECTS) { + redirectLimitReached = true; + } + continue; + } + + String contentType = connection.getContentType(); + String html = ""; + InputStream stream = status >= 400 ? connection.getErrorStream() : connection.getInputStream(); + if (stream != null && contentType != null && contentType.toLowerCase().contains("text/html")) { + html = readLimited(stream); + } + + return new PhishingFetchResult( + inputUrl, + currentUrl, + contentType == null ? "" : contentType, + html, + redirectChain, + true, + redirectLoop, + redirectLimitReached + ); + } catch (Exception ignored) { + break; + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + return new PhishingFetchResult( + inputUrl, + currentUrl, + "", + "", + redirectChain, + false, + redirectLoop, + redirectLimitReached + ); + } + + private static boolean isRedirect(int status) { + return status == 301 || status == 302 || status == 303 || status == 307 || status == 308; + } + + private static String ensureScheme(String inputUrl) { + String value = PhishingURLParser.normalizeForAnalysis(inputUrl); + if (value.startsWith("http://") || value.startsWith("https://")) { + return value; + } + return "https://" + value; + } + + private static String readLimited(InputStream stream) throws Exception { + try (InputStream in = stream; ByteArrayOutputStream out = new ByteArrayOutputStream()) { + byte[] buffer = new byte[4096]; + int total = 0; + int read; + while ((read = in.read(buffer)) != -1 && total < MAX_HTML_BYTES) { + int toWrite = Math.min(read, MAX_HTML_BYTES - total); + out.write(buffer, 0, toWrite); + total += toWrite; + } + return out.toString(StandardCharsets.UTF_8); + } + } +} diff --git a/Team 87 - PassVault/password-manager/PhishingPageHandler.java b/Team 87 - PassVault/password-manager/PhishingPageHandler.java new file mode 100644 index 000000000..f395937a9 --- /dev/null +++ b/Team 87 - PassVault/password-manager/PhishingPageHandler.java @@ -0,0 +1,59 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class PhishingPageHandler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String sessionId = WebUtils.getSessionId(exchange); + String email = SessionManager.getUser(sessionId); + + if (email == null) { + WebUtils.redirect(exchange, "/"); + return; + } + + String prefillUrl = getQueryValue(exchange.getRequestURI().getQuery(), "url"); + + String html = new String(Files.readAllBytes(Paths.get("web/phishing.html")), StandardCharsets.UTF_8); + html = html.replace("{{PREFILL_URL}}", WebUtils.escapeHtml(prefillUrl)); + + byte[] response = html.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Cache-Control", "no-cache, no-store, must-revalidate"); + exchange.getResponseHeaders().set("Pragma", "no-cache"); + exchange.getResponseHeaders().set("Expires", "0"); + exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8"); + exchange.sendResponseHeaders(200, response.length); + exchange.getResponseBody().write(response); + exchange.close(); + } catch (Exception e) { + e.printStackTrace(); + String err = "Internal Server Error"; + exchange.sendResponseHeaders(500, err.length()); + exchange.getResponseBody().write(err.getBytes(StandardCharsets.UTF_8)); + exchange.close(); + } + } + + private String getQueryValue(String query, String key) throws IOException { + if (query == null || query.isBlank()) { + return ""; + } + + for (String pair : query.split("&")) { + String[] kv = pair.split("=", 2); + String currentKey = URLDecoder.decode(kv[0], StandardCharsets.UTF_8); + if (currentKey.equals(key)) { + return kv.length > 1 ? URLDecoder.decode(kv[1], StandardCharsets.UTF_8) : ""; + } + } + + return ""; + } +} diff --git a/Team 87 - PassVault/password-manager/PhishingScan.java b/Team 87 - PassVault/password-manager/PhishingScan.java new file mode 100644 index 000000000..cb4514b89 --- /dev/null +++ b/Team 87 - PassVault/password-manager/PhishingScan.java @@ -0,0 +1,21 @@ +public class PhishingScan { + private final String url; + private final int score; + private final String verdict; + private final String detail; + private final String scannedAt; + + public PhishingScan(String url, int score, String verdict, String detail, String scannedAt) { + this.url = url; + this.score = score; + this.verdict = verdict; + this.detail = detail; + this.scannedAt = scannedAt; + } + + public String getUrl() { return url; } + public int getScore() { return score; } + public String getVerdict() { return verdict; } + public String getDetail() { return detail; } + public String getScannedAt() { return scannedAt; } +} diff --git a/Team 87 - PassVault/password-manager/PhishingScoreEngine.java b/Team 87 - PassVault/password-manager/PhishingScoreEngine.java new file mode 100644 index 000000000..6e723c4df --- /dev/null +++ b/Team 87 - PassVault/password-manager/PhishingScoreEngine.java @@ -0,0 +1,34 @@ +public class PhishingScoreEngine { + + public static String getVerdict(int score) { + return getVerdict(score, false); + } + + public static String getVerdict(int score, boolean plainHttpTrustedDomain) { + if (plainHttpTrustedDomain) return "SLIGHTLY SUSPICIOUS"; + if (score <= 0) return "SAFE"; + if (score <= 5) return "SUSPICIOUS"; + if (score <= 10) return "HIGH RISK"; + return "VERY HIGH RISK"; + } + + public static String getVerdictDetail(int score) { + return getVerdictDetail(score, false); + } + + public static String getVerdictDetail(int score, boolean plainHttpTrustedDomain) { + if (plainHttpTrustedDomain) { + return "The domain is trusted, but it is using plain HTTP instead of HTTPS. Proceed with caution."; + } + if (score <= 0) { + return "This URL appears to be legitimate and safe to visit."; + } + if (score <= 5) { + return "This URL has some suspicious traits. Proceed with caution."; + } + if (score <= 10) { + return "This URL shows strong signs of phishing. Avoid visiting it."; + } + return "This URL is almost certainly a phishing attempt. Do not visit it."; + } +} diff --git a/Team 87 - PassVault/password-manager/PhishingService.java b/Team 87 - PassVault/password-manager/PhishingService.java new file mode 100644 index 000000000..e025ddf60 --- /dev/null +++ b/Team 87 - PassVault/password-manager/PhishingService.java @@ -0,0 +1,88 @@ +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PhishingService { + + public static PhishingAnalysisResult analyzeAndStore(String email, String url) throws Exception { + PhishingDetectorEngine detector = new PhishingDetectorEngine(); + PhishingAnalysisResult result = detector.analyze(url); + saveScan(email, result); + return result; + } + + public static List getRecentScans(String email, int limit) throws Exception { + List scans = new ArrayList<>(); + String sql = + "SELECT url, score, verdict, detail, DATE_FORMAT(scanned_at, '%Y-%m-%d %H:%i:%s') AS scanned_at_display " + + "FROM phishing_scans WHERE user_email = ? ORDER BY scanned_at DESC, id DESC LIMIT ?"; + + try (Connection conn = DBConnection.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, email); + ps.setInt(2, limit); + + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + scans.add(new PhishingScan( + rs.getString("url"), + rs.getInt("score"), + rs.getString("verdict"), + rs.getString("detail"), + rs.getString("scanned_at_display") + )); + } + } + } + + return scans; + } + + public static Map getStats(String email) throws Exception { + Map stats = new HashMap<>(); + stats.put("total", 0); + stats.put("highRisk", 0); + + String sql = + "SELECT COUNT(*) AS total, " + + "SUM(CASE WHEN score >= 6 THEN 1 ELSE 0 END) AS high_risk " + + "FROM phishing_scans WHERE user_email = ?"; + + try (Connection conn = DBConnection.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, email); + + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + stats.put("total", rs.getInt("total")); + stats.put("highRisk", rs.getInt("high_risk")); + } + } + } + + return stats; + } + + private static void saveScan(String email, PhishingAnalysisResult result) throws Exception { + String sql = + "INSERT INTO phishing_scans (user_email, url, score, verdict, detail, reasons) VALUES (?, ?, ?, ?, ?, ?)"; + + try (Connection conn = DBConnection.getConnection(); + PreparedStatement ps = conn.prepareStatement(sql)) { + + ps.setString(1, email); + ps.setString(2, result.getUrl()); + ps.setInt(3, result.getScore()); + ps.setString(4, result.getVerdict()); + ps.setString(5, result.getDetail()); + ps.setString(6, String.join("\n", result.getReasons())); + ps.executeUpdate(); + } + } +} diff --git a/Team 87 - PassVault/password-manager/PhishingTrie.java b/Team 87 - PassVault/password-manager/PhishingTrie.java new file mode 100644 index 000000000..f88133a3a --- /dev/null +++ b/Team 87 - PassVault/password-manager/PhishingTrie.java @@ -0,0 +1,46 @@ +public class PhishingTrie { + + private static class TrieNode { + TrieNode[] children = new TrieNode[128]; + boolean isEndOfWord; + } + + private final TrieNode root; + + public PhishingTrie() { + this.root = new TrieNode(); + } + + public void insert(String domain) { + TrieNode current = root; + String normalized = domain.toLowerCase().trim(); + + for (char ch : normalized.toCharArray()) { + int index = ch; + if (index < 0 || index >= 128) { + continue; + } + if (current.children[index] == null) { + current.children[index] = new TrieNode(); + } + current = current.children[index]; + } + + current.isEndOfWord = true; + } + + public boolean search(String domain) { + TrieNode current = root; + String normalized = domain.toLowerCase().trim(); + + for (char ch : normalized.toCharArray()) { + int index = ch; + if (index < 0 || index >= 128 || current.children[index] == null) { + return false; + } + current = current.children[index]; + } + + return current.isEndOfWord; + } +} diff --git a/Team 87 - PassVault/password-manager/PhishingURLParser.java b/Team 87 - PassVault/password-manager/PhishingURLParser.java new file mode 100644 index 000000000..4d153a49f --- /dev/null +++ b/Team 87 - PassVault/password-manager/PhishingURLParser.java @@ -0,0 +1,82 @@ +public class PhishingURLParser { + + public String protocol = ""; + public String domain = ""; + public String subdomain = ""; + public String rootDomain = ""; + public String tld = ""; + public String path = ""; + public String rawInput = ""; + + public void parse(String url) { + rawInput = url == null ? "" : url.trim(); + String value = normalizeForAnalysis(rawInput).toLowerCase(); + + if (value.startsWith("https://")) { + protocol = "https"; + value = value.substring(8); + } else if (value.startsWith("http://")) { + protocol = "http"; + value = value.substring(7); + } else if (value.startsWith("ftp://")) { + protocol = "ftp"; + value = value.substring(6); + } else { + protocol = "https"; + } + + int queryIndex = value.indexOf('?'); + if (queryIndex != -1) { + value = value.substring(0, queryIndex); + } + + int slashIndex = value.indexOf('/'); + if (slashIndex != -1) { + domain = value.substring(0, slashIndex); + path = value.substring(slashIndex); + } else { + domain = value; + path = ""; + } + + int portIndex = domain.indexOf(':'); + if (portIndex != -1) { + domain = domain.substring(0, portIndex); + } + + int lastDot = domain.lastIndexOf('.'); + if (lastDot != -1) { + tld = domain.substring(lastDot); + String withoutTld = domain.substring(0, lastDot); + int secondLastDot = withoutTld.lastIndexOf('.'); + + if (secondLastDot != -1) { + subdomain = withoutTld.substring(0, secondLastDot); + rootDomain = withoutTld.substring(secondLastDot + 1); + } else { + subdomain = ""; + rootDomain = withoutTld; + } + } else { + tld = ""; + subdomain = ""; + rootDomain = domain; + } + } + + public static String normalizeForAnalysis(String url) { + if (url == null) { + return ""; + } + + String normalized = url.trim(); + normalized = normalized.replace("[.]", "."); + normalized = normalized.replace("(.)", "."); + normalized = normalized.replace("{.}", "."); + normalized = normalized.replace("hxxps://", "https://"); + normalized = normalized.replace("hxxp://", "http://"); + normalized = normalized.replace("hxxps:", "https:"); + normalized = normalized.replace("hxxp:", "http:"); + return normalized; + } +} diff --git a/Team 87 - PassVault/password-manager/RegisterHandler.java b/Team 87 - PassVault/password-manager/RegisterHandler.java new file mode 100644 index 000000000..a45fa187d --- /dev/null +++ b/Team 87 - PassVault/password-manager/RegisterHandler.java @@ -0,0 +1,30 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.util.Map; + +public class RegisterHandler implements HttpHandler { + + public void handle(HttpExchange exchange) throws IOException { + try { + if (!exchange.getRequestMethod().equalsIgnoreCase("POST")) { + exchange.sendResponseHeaders(405, -1); + return; + } + + Map form = RequestUtil.parseFormBody(exchange); + String email = form.getOrDefault("email", ""); + String password = form.getOrDefault("password", ""); + + PasswordManager.register(email, password); + WebUtils.redirectWithFlash(exchange, "/", "register", "success", "Registration successful. Please log in."); + } catch (AuthException e) { + WebUtils.redirectWithFlash(exchange, "/", "register", "error", e.getMessage()); + } catch (Exception e) { + e.printStackTrace(); + WebUtils.redirectWithFlash(exchange, "/", "register", "error", "Registration failed. Please try again."); + } finally { + exchange.close(); + } + } +} diff --git a/Team 87 - PassVault/password-manager/RequestUtil.java b/Team 87 - PassVault/password-manager/RequestUtil.java new file mode 100644 index 000000000..d09bd6970 --- /dev/null +++ b/Team 87 - PassVault/password-manager/RequestUtil.java @@ -0,0 +1,39 @@ +import com.sun.net.httpserver.HttpExchange; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +public class RequestUtil { + + public static Map parseFormBody(HttpExchange exchange) throws IOException { + String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + return parseUrlEncoded(body); + } + + public static Map parseQuery(String query) { + return parseUrlEncoded(query); + } + + private static Map parseUrlEncoded(String raw) { + Map values = new LinkedHashMap<>(); + + if (raw == null || raw.isEmpty()) { + return values; + } + + for (String pair : raw.split("&")) { + if (pair.isEmpty()) { + continue; + } + + String[] kv = pair.split("=", 2); + String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8); + String value = kv.length > 1 ? URLDecoder.decode(kv[1], StandardCharsets.UTF_8) : ""; + values.put(key, value); + } + + return values; + } +} diff --git a/Team 87 - PassVault/password-manager/RevealHandler.java b/Team 87 - PassVault/password-manager/RevealHandler.java new file mode 100644 index 000000000..a37b2224e --- /dev/null +++ b/Team 87 - PassVault/password-manager/RevealHandler.java @@ -0,0 +1,43 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class RevealHandler implements HttpHandler { + + public void handle(HttpExchange exchange) throws IOException { + try { + String session = SessionManager.extractSessionId( + exchange.getRequestHeaders().getFirst("Cookie") + ); + String vaultKey = SessionManager.getVaultKey(session); + + if (vaultKey == null) { + exchange.sendResponseHeaders(401, -1); + return; + } + + Map query = RequestUtil.parseQuery(exchange.getRequestURI().getQuery()); + String encrypted = query.get("data"); + + if (encrypted == null) { + throw new Exception("Missing data"); + } + + String decrypted = EncryptionUtil.decrypt(encrypted, vaultKey); + + byte[] response = decrypted.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, response.length); + exchange.getResponseBody().write(response); + } catch (Exception e) { + e.printStackTrace(); + + String err = "Decryption failed"; + exchange.sendResponseHeaders(500, err.length()); + exchange.getResponseBody().write(err.getBytes(StandardCharsets.UTF_8)); + } finally { + exchange.close(); + } + } +} diff --git a/Team 87 - PassVault/password-manager/SessionManager.java b/Team 87 - PassVault/password-manager/SessionManager.java new file mode 100644 index 000000000..aeab5797a --- /dev/null +++ b/Team 87 - PassVault/password-manager/SessionManager.java @@ -0,0 +1,71 @@ +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +public class SessionManager { + + public static final class SessionData { + private final String email; + private final String vaultKey; + + public SessionData(String email, String vaultKey) { + this.email = email; + this.vaultKey = vaultKey; + } + + public String getEmail() { + return email; + } + + public String getVaultKey() { + return vaultKey; + } + } + + private static final Map sessions = new ConcurrentHashMap<>(); + + public static String createSession(String email, String vaultKey) { + String session = UUID.randomUUID().toString(); + sessions.put(session, new SessionData(email, vaultKey)); + return session; + } + + public static SessionData getSession(String session) { + if (session == null || session.isEmpty()) { + return null; + } + + return sessions.get(session); + } + + public static String getUser(String session) { + SessionData data = getSession(session); + return data == null ? null : data.getEmail(); + } + + public static String getVaultKey(String session) { + SessionData data = getSession(session); + return data == null ? null : data.getVaultKey(); + } + + public static void removeSession(String session) { + if (session != null && !session.isEmpty()) { + sessions.remove(session); + } + } + + public static String extractSessionId(String cookieHeader) { + if (cookieHeader == null || cookieHeader.isEmpty()) { + return null; + } + + for (String cookie : cookieHeader.split(";")) { + String[] parts = cookie.trim().split("=", 2); + if (parts.length == 2 && parts[0].equals("session")) { + return parts[1]; + } + } + + return null; + } +} diff --git a/Team 87 - PassVault/password-manager/TwoFactorPageHandler.java b/Team 87 - PassVault/password-manager/TwoFactorPageHandler.java new file mode 100644 index 000000000..ba1f6e93d --- /dev/null +++ b/Team 87 - PassVault/password-manager/TwoFactorPageHandler.java @@ -0,0 +1,69 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class TwoFactorPageHandler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + if (!exchange.getRequestMethod().equalsIgnoreCase("GET")) { + exchange.sendResponseHeaders(405, -1); + return; + } + + Map query = RequestUtil.parseQuery(exchange.getRequestURI().getRawQuery()); + String token = query.getOrDefault("token", ""); + PendingLogin pendingLogin = PendingLoginStore.get(token); + + if (pendingLogin == null) { + sendHtml(exchange, 404, page("Verification expired", "

Please log in again.

Back to login")); + return; + } + + String safeToken = WebUtils.escapeHtml(token); + String safeCode = WebUtils.escapeHtml(pendingLogin.getCode()); + String speed = pendingLogin.getRiskResult().getSpeedKmph() == null + ? "unknown" + : String.format("%.2f km/hr", pendingLogin.getRiskResult().getSpeedKmph()); + + String body = + "

A high-risk login was detected from a new location. Complete verification before opening the vault.

" + + "
" + + "
Demo verification code
" + + "" + safeCode + "" + + "

No email/SMS API is used in this project, so the demo code is shown here. Production apps deliver this by email, SMS, or authenticator app.

" + + "

Estimated travel speed: " + WebUtils.escapeHtml(speed) + "

" + + "
" + + "
" + + "" + + "" + + "" + + "
"; + + sendHtml(exchange, 200, page("Two-Factor Verification", body)); + } catch (Exception e) { + e.printStackTrace(); + sendHtml(exchange, 500, page("Verification failed", "

Please try logging in again.

")); + } finally { + exchange.close(); + } + } + + private static String page(String title, String body) { + return "" + WebUtils.escapeHtml(title) + "" + + "" + + "
PassVault
" + + "

" + WebUtils.escapeHtml(title) + "

" + body + "
" + + ""; + } + + private static void sendHtml(HttpExchange exchange, int statusCode, String html) throws IOException { + byte[] data = html.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "text/html; charset=UTF-8"); + exchange.sendResponseHeaders(statusCode, data.length); + exchange.getResponseBody().write(data); + } +} diff --git a/Team 87 - PassVault/password-manager/TwoFactorVerifyHandler.java b/Team 87 - PassVault/password-manager/TwoFactorVerifyHandler.java new file mode 100644 index 000000000..ffc9cc1f5 --- /dev/null +++ b/Team 87 - PassVault/password-manager/TwoFactorVerifyHandler.java @@ -0,0 +1,52 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +public class TwoFactorVerifyHandler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + if (!exchange.getRequestMethod().equalsIgnoreCase("POST")) { + exchange.sendResponseHeaders(405, -1); + return; + } + + Map form = RequestUtil.parseFormBody(exchange); + PendingLogin pendingLogin = LoginSecurityManager.completePendingLogin( + form.getOrDefault("token", ""), + form.getOrDefault("code", "") + ); + + if (pendingLogin == null) { + sendText(exchange, 401, "Invalid verification code. Please go back and try again."); + return; + } + + String session = SessionManager.createSession( + pendingLogin.getEmail(), + pendingLogin.getVaultKey() + ); + exchange.getResponseHeaders().add( + "Set-Cookie", + "session=" + session + "; Path=/; HttpOnly; SameSite=Lax" + ); + exchange.getResponseHeaders().add("Location", "/dashboard"); + exchange.sendResponseHeaders(302, -1); + } catch (Exception e) { + e.printStackTrace(); + sendText(exchange, 500, "Verification failed"); + } finally { + exchange.close(); + } + } + + private static void sendText(HttpExchange exchange, int statusCode, String text) throws IOException { + byte[] data = text.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8"); + exchange.sendResponseHeaders(statusCode, data.length); + exchange.getResponseBody().write(data); + } +} diff --git a/Team 87 - PassVault/password-manager/User.java b/Team 87 - PassVault/password-manager/User.java new file mode 100644 index 000000000..e69de29bb diff --git a/Team 87 - PassVault/password-manager/VaultHandler.java b/Team 87 - PassVault/password-manager/VaultHandler.java new file mode 100644 index 000000000..9018810f2 --- /dev/null +++ b/Team 87 - PassVault/password-manager/VaultHandler.java @@ -0,0 +1,99 @@ +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +public class VaultHandler implements HttpHandler { + + @Override + public void handle(HttpExchange exchange) throws IOException { + try { + String sessionId = SessionManager.extractSessionId( + exchange.getRequestHeaders().getFirst("Cookie") + ); + + if (sessionId == null) { + exchange.getResponseHeaders().add("Location", "/"); + exchange.sendResponseHeaders(302, -1); + return; + } + + String email = SessionManager.getUser(sessionId); + + if (email == null) { + exchange.getResponseHeaders().add("Location", "/"); + exchange.sendResponseHeaders(302, -1); + return; + } + + List list = PasswordManager.getPasswords(email); + StringBuilder rows = new StringBuilder(); + + for (PasswordEntry p : list) { + String strengthClass = p.getStrength().toLowerCase(); + String safeWebsite = WebUtils.escapeHtml(p.getWebsite()); + String safeUsername = WebUtils.escapeHtml(p.getUsername()); + String safeStrength = WebUtils.escapeHtml(p.getStrength()); + String encodedWebsite = URLEncoder.encode(p.getWebsite(), StandardCharsets.UTF_8); + + rows.append("") + .append("").append(safeWebsite).append("") + .append("").append(safeUsername).append("") + .append("******") + .append("") + .append(safeStrength) + .append("") + .append("") + .append("") + .append("Scan") + .append("
") + .append("") + .append("") + .append("") + .append("
") + .append("") + .append(""); + } + + String html = new String( + Files.readAllBytes(Paths.get("web/vault.html")), + StandardCharsets.UTF_8 + ); + + html = html.replace("{{ROWS}}", rows.toString()); + + exchange.getResponseHeaders().set("Cache-Control", "no-cache, no-store, must-revalidate"); + exchange.getResponseHeaders().set("Pragma", "no-cache"); + exchange.getResponseHeaders().set("Expires", "0"); + exchange.getResponseHeaders().set("Content-Type", "text/html; charset=utf-8"); + + byte[] response = html.getBytes(StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, response.length); + + try (OutputStream os = exchange.getResponseBody()) { + os.write(response); + } + } catch (Exception e) { + e.printStackTrace(); + + String err = "Internal Server Error"; + exchange.sendResponseHeaders(500, err.length()); + exchange.getResponseBody().write(err.getBytes(StandardCharsets.UTF_8)); + } finally { + exchange.close(); + } + } +} diff --git a/Team 87 - PassVault/password-manager/WebUtils.java b/Team 87 - PassVault/password-manager/WebUtils.java new file mode 100644 index 000000000..7f5a584c1 --- /dev/null +++ b/Team 87 - PassVault/password-manager/WebUtils.java @@ -0,0 +1,98 @@ +import com.sun.net.httpserver.HttpExchange; +import java.io.IOException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; + +public class WebUtils { + + public static Map parseFormBody(String body) throws IOException { + Map values = new LinkedHashMap<>(); + if (body == null || body.isBlank()) { + return values; + } + + for (String pair : body.split("&")) { + String[] kv = pair.split("=", 2); + String key = URLDecoder.decode(kv[0], StandardCharsets.UTF_8); + String value = kv.length > 1 ? URLDecoder.decode(kv[1], StandardCharsets.UTF_8) : ""; + values.put(key, value); + } + + return values; + } + + public static String getSessionId(HttpExchange exchange) { + String cookie = exchange.getRequestHeaders().getFirst("Cookie"); + if (cookie == null) { + return null; + } + + for (String part : cookie.split(";")) { + String trimmed = part.trim(); + if (trimmed.startsWith("session=")) { + String[] pair = trimmed.split("=", 2); + return pair.length > 1 ? pair[1] : null; + } + } + + return null; + } + + public static void redirect(HttpExchange exchange, String location) throws IOException { + exchange.getResponseHeaders().add("Location", location); + exchange.sendResponseHeaders(302, -1); + exchange.close(); + } + + public static void redirectWithFlash(HttpExchange exchange, String location, String form, String type, String message) throws IOException { + String separator = location.contains("?") ? "&" : "?"; + String target = + location + + separator + + "form=" + urlEncode(form) + + "&type=" + urlEncode(type) + + "&message=" + urlEncode(message); + redirect(exchange, target); + } + + public static String escapeHtml(String value) { + if (value == null) { + return ""; + } + + return value + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } + + public static String jsonEscape(String value) { + if (value == null) { + return ""; + } + + return value + .replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\r", "\\r") + .replace("\n", "\\n") + .replace("\t", "\\t"); + } + + public static void sendJson(HttpExchange exchange, int statusCode, String json) throws IOException { + byte[] data = json.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(statusCode, data.length); + exchange.getResponseBody().write(data); + exchange.close(); + } + + private static String urlEncode(String value) { + return URLEncoder.encode(value, StandardCharsets.UTF_8); + } +} diff --git a/Team 87 - PassVault/password-manager/lib/mysql-connector-j-9.6.0.jar b/Team 87 - PassVault/password-manager/lib/mysql-connector-j-9.6.0.jar new file mode 100644 index 000000000..9c62b72fe Binary files /dev/null and b/Team 87 - PassVault/password-manager/lib/mysql-connector-j-9.6.0.jar differ diff --git a/Team 87 - PassVault/password-manager/run.sh b/Team 87 - PassVault/password-manager/run.sh new file mode 100644 index 000000000..0006c4107 --- /dev/null +++ b/Team 87 - PassVault/password-manager/run.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +JAR="lib/mysql-connector-j-9.6.0.jar" + +if [ ! -f "$JAR" ]; then + echo "Missing MySQL JDBC driver: $JAR" + exit 1 +fi + +if command -v pkill >/dev/null 2>&1; then + pkill -f "java -cp .*Main" >/dev/null 2>&1 || true +fi + +javac -cp ".:$JAR" *.java +java -cp ".:$JAR" Main diff --git a/Team 87 - PassVault/password-manager/web/dashboard.html b/Team 87 - PassVault/password-manager/web/dashboard.html new file mode 100644 index 000000000..b125fc261 --- /dev/null +++ b/Team 87 - PassVault/password-manager/web/dashboard.html @@ -0,0 +1,42 @@ + + + + Dashboard + + + + + + +
+ +

Dashboard

+ +
+

Total Passwords

+

{{TOTAL}}

+
+ +
+

Weak Passwords

+

{{WEAK}}

+
+ +
+

Cybersecurity Tools

+

Analyze suspicious links with the DSA-powered phishing detector.

+ Open Phishing Detector +
+ +
+ + + diff --git a/Team 87 - PassVault/password-manager/web/index.html b/Team 87 - PassVault/password-manager/web/index.html new file mode 100644 index 000000000..3fbf9ca4b --- /dev/null +++ b/Team 87 - PassVault/password-manager/web/index.html @@ -0,0 +1,84 @@ + + + + PassVault Login + + + + + + +
+
+

Login

+

Use a real email like name@gmail.com and a password with at least 8 characters.

+ + +
+ + + + +

Checking login location securely...

+ +
+ +
+ +

Register

+
+ + + +
+
+
+ + + + + diff --git a/Team 87 - PassVault/password-manager/web/logged-out.html b/Team 87 - PassVault/password-manager/web/logged-out.html new file mode 100644 index 000000000..ce2fdf2da --- /dev/null +++ b/Team 87 - PassVault/password-manager/web/logged-out.html @@ -0,0 +1,25 @@ + + + + Logged Out + + + + + + +
+
+

You have been logged out

+

Your session has been cleared safely.

+ Back to login +
+
+ + + diff --git a/Team 87 - PassVault/password-manager/web/phishing.html b/Team 87 - PassVault/password-manager/web/phishing.html new file mode 100644 index 000000000..220065aed --- /dev/null +++ b/Team 87 - PassVault/password-manager/web/phishing.html @@ -0,0 +1,93 @@ + + + + Phishing Detector + + + + + + +
+

Phishing Detector

+ +
+

Analyze Suspicious URLs

+

Check whether a URL looks safe or like a phishing attempt before you trust it.

+
+ + +
+
+ + +
+ + + + + diff --git a/Team 87 - PassVault/password-manager/web/style.css b/Team 87 - PassVault/password-manager/web/style.css new file mode 100644 index 000000000..198073a18 --- /dev/null +++ b/Team 87 - PassVault/password-manager/web/style.css @@ -0,0 +1,390 @@ +/* RESET */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Inter', 'Segoe UI', sans-serif; +} + +/* BACKGROUND */ +body { + display: flex; + background: radial-gradient(circle at top, #0f172a, #020617); + color: #e5e7eb; +} + +/* SIDEBAR */ +.sidebar { + width: 240px; + height: 100vh; + background: #020617; + padding: 25px; + border-right: 1px solid rgba(255,255,255,0.05); +} + +.logo { + font-size: 18px; + font-weight: 600; + color: #60a5fa; + margin-bottom: 40px; +} + +.nav a { + display: block; + padding: 12px; + margin-bottom: 10px; + border-radius: 8px; + color: #94a3b8; + text-decoration: none; + transition: 0.2s; +} + +.nav a:hover { + background: rgba(96,165,250,0.1); + color: #60a5fa; +} + +/* MAIN */ +.main { + flex: 1; + padding: 40px; + max-width: 1100px; + margin: auto; +} + +h2 { + margin-bottom: 20px; + font-weight: 600; +} + +.auth-hint { + margin-bottom: 14px; + color: #94a3b8; + line-height: 1.5; +} + +/* CARDS */ +.card { + background: rgba(15, 23, 42, 0.6); + border-radius: 14px; + padding: 20px; + margin-bottom: 20px; + border: 1px solid rgba(255,255,255,0.04); +} + +.action-link { + display: inline-block; + padding: 10px 14px; + border-radius: 6px; + background: #2563eb; + color: white; + text-decoration: none; + font-size: 13px; +} + +.action-link:hover { + background: #1d4ed8; +} + +.helper-text { + margin-top: 10px; + color: #94a3b8; + font-size: 13px; +} + +.status-message { + margin-bottom: 16px; + padding: 12px 14px; + border-radius: 10px; + border: 1px solid transparent; + font-size: 14px; +} + +.status-error { + background: rgba(239, 68, 68, 0.12); + border-color: rgba(239, 68, 68, 0.35); + color: #fecaca; +} + +.status-success { + background: rgba(34, 197, 94, 0.12); + border-color: rgba(34, 197, 94, 0.35); + color: #bbf7d0; +} + +.location-status { + margin-top: 10px; + color: #94a3b8; + font-size: 12px; +} + +.alert-card { + background: rgba(234,179,8,0.12); + border: 1px solid rgba(234,179,8,0.24); + border-radius: 12px; + margin: 16px 0; + padding: 16px; +} + +.alert-card strong { + display: block; + color: #facc15; + font-size: 28px; + letter-spacing: 4px; + margin-top: 8px; +} + +/* INPUTS */ +input { + width: 100%; + padding: 10px; + margin-top: 8px; + background: #020617; + border: 1px solid rgba(255,255,255,0.05); + border-radius: 6px; + color: #e5e7eb; + font-size: 14px; +} + +input:focus { + outline: none; + border-color: #2563eb; +} + +/* BUTTONS */ +button { + padding: 8px 14px; + border-radius: 6px; + border: none; + background: #2563eb; + color: white; + font-size: 13px; + cursor: pointer; + transition: 0.2s; +} + +button:hover { + background: #1d4ed8; +} + +/* ROW LAYOUT */ +.row { + display: flex; + gap: 10px; + align-items: center; +} + +/* SEARCH */ +.search { + margin-bottom: 20px; +} + +/* GENERATOR */ +.generated-box { + margin-top: 10px; + background: #020617; + padding: 10px; + border-radius: 6px; + display: flex; + justify-content: space-between; +} + +.generated-box span { + color: #60a5fa; + font-size: 14px; +} + +.phishing-hero { + border: 1px solid rgba(96,165,250,0.18); +} + +.phishing-input-row { + align-items: stretch; +} + +.phishing-input-row input { + margin-top: 0; +} + +.phishing-verdict { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + border-left: 6px solid transparent; +} + +.verdict-safe { border-left-color: #22c55e; } +.verdict-suspicious { border-left-color: #eab308; } +.verdict-high { border-left-color: #ef4444; } +.verdict-critical { border-left-color: #a855f7; } + +.eyebrow { + color: #94a3b8; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 1px; +} + +.score-pill { + min-width: 78px; + height: 78px; + border-radius: 999px; + background: rgba(2, 6, 23, 0.9); + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; + font-weight: 700; + color: #60a5fa; +} + +/* TABLE */ +table { + width: 100%; + border-collapse: collapse; + margin-top: 10px; +} + +th { + text-align: left; + padding: 10px; + font-size: 13px; + color: #64748b; + font-weight: 500; +} + +td { + padding: 12px 10px; + border-bottom: 1px solid rgba(255,255,255,0.04); + font-size: 14px; + word-break: break-word; +} + +/* ROW HOVER */ +tr:hover { + background: rgba(255,255,255,0.02); +} + +/* PASSWORD STYLE */ +td[data-revealed="false"] { + color: #64748b; + letter-spacing: 2px; +} + +td[data-revealed="true"] { + color: #e5e7eb; +} + +/* BADGES */ +.badge { + padding: 4px 8px; + border-radius: 6px; + font-size: 11px; + font-weight: 500; +} + +.strong { background: rgba(34,197,94,0.15); color: #22c55e; } +.medium { background: rgba(234,179,8,0.15); color: #eab308; } +.weak { background: rgba(239,68,68,0.15); color: #ef4444; } +.critical { background: rgba(168,85,247,0.18); color: #d8b4fe; } + +/* ACTION AREA */ +td button { + margin-right: 6px; +} + +td form { + display: inline-flex; + gap: 5px; +} + +td form input { + width: 90px; + padding: 6px; + font-size: 12px; +} + +.components-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; +} + +.comp-item { + background: #020617; + border-radius: 10px; + padding: 12px; +} + +.comp-label { + display: block; + color: #64748b; + font-size: 11px; + text-transform: uppercase; + margin-bottom: 4px; +} + +.comp-value { + color: #e5e7eb; + font-size: 14px; +} + +.reasons-list { + list-style: none; + display: flex; + flex-direction: column; + gap: 10px; +} + +.reason-item { + display: flex; + gap: 12px; + align-items: center; + padding: 12px; + border-radius: 10px; +} + +.risk-reason { + background: rgba(239,68,68,0.12); + color: #fecaca; +} + +.safe-reason { + background: rgba(34,197,94,0.12); + color: #bbf7d0; +} + +.reason-num { + width: 26px; + height: 26px; + min-width: 26px; + border-radius: 999px; + background: rgba(255,255,255,0.08); + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 12px; +} + +.dsa-card { + border: 1px solid rgba(96,165,250,0.18); +} + +.dsa-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 12px; +} + +.dsa-item { + background: #020617; + border-radius: 10px; + padding: 14px; +} + +.dsa-item strong { + display: block; + color: #60a5fa; + margin-bottom: 6px; +} diff --git a/Team 87 - PassVault/password-manager/web/vault.html b/Team 87 - PassVault/password-manager/web/vault.html new file mode 100644 index 000000000..57ae5e0e4 --- /dev/null +++ b/Team 87 - PassVault/password-manager/web/vault.html @@ -0,0 +1,161 @@ + + + + Vault + + + + + + +
+ +

Vault

+ + + + + +
+

Password Generator

+ +
+ + + + + + + +
+ +

+ 12+ characters recommended for strong passwords +

+ +
+ Click generate + +
+
+ + +
+

Add Password

+
+ + + + +
+

Unsure about a link? Use the phishing detector before saving credentials for that site.

+
+ + +
+ + + + + + + + + + {{ROWS}} + +
WebsiteUsernamePasswordStrengthActions
+
+ +
+ + + + +