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
+
+
+ - Secure Signup/Login using PBKDF2 (with salting)
+ - Password Strength Checker (Weak / Medium / Strong)
+ - Password Generator with customizable options
+ - Encrypted Password Storage using AES
+ - Weak Password Detection across accounts
+ - Login Alert System (time + location tracking)
+ - Phishing Website Detection using URL analysis
+
+
+
+
+ DSA Concepts Used
+
+
+ - Hashing → Secure user authentication
+ - Linked List → Managing dynamic user data
+ - Trees → URL structure parsing in phishing detection
+ - Strings / Arrays → Password analysis and generation
+
+
+
+
+ Tech Stack
+
+
+ - Frontend: HTML and CSS
+ - Backend: JAVA
+ - Database: MySQL
+
+
+
+
+ How It Works
+
+
+ - User signs up and logs in
+ - Dashboard provides security tools
+ - Passwords are stored securely in encrypted form
+ - System analyzes passwords and detects threats
+
+
+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("