flintbase 0.3.1

Google / Firebase API key analyzer and APK secret scanner — tests keys against 20+ endpoints and extracts hardcoded credentials from Android apps
use reqwest::blocking::Client;
use serde_json::Value;

use crate::config::{FirebaseProjectInfo, TestResult};

/// Test if anonymous signup is enabled — reveals auth configuration.
pub fn test_anonymous_signup(
    client: &Client,
    key: &str,
    info: &mut FirebaseProjectInfo,
) -> TestResult {
    let url = format!(
        "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key={}",
        key
    );

    let resp = match client
        .post(&url)
        .json(&serde_json::json!({"returnSecureToken": true}))
        .send()
    {
        Ok(r) => r,
        Err(e) => return TestResult::fail(None, e.to_string(), None),
    };

    let status = resp.status().as_u16();
    let data: Value = resp.json().unwrap_or_default();

    if status == 200 {
        info.anonymous_auth_enabled = Some(true);
        let mut result = TestResult::ok(status, "Anonymous signup ENABLED — got valid idToken");
        if let Some(id) = data.get("localId").and_then(|v| v.as_str()) {
            result = result.with_extra("localId", id);
        }
        if let Some(tok) = data.get("idToken").and_then(|v| v.as_str()) {
            let preview = if tok.len() > 50 {
                format!("{}...", &tok[..50])
            } else {
                tok.to_string()
            };
            result = result.with_extra("idToken", preview);
        }
        result
    } else if data.to_string().contains("OPERATION_NOT_ALLOWED") {
        info.anonymous_auth_enabled = Some(false);
        TestResult::fail(
            Some(status),
            "OPERATION_NOT_ALLOWED",
            Some("Anonymous auth is DISABLED for this project".into()),
        )
    } else {
        let msg = extract_error_message(&data);
        TestResult::fail(Some(status), msg, None)
    }
}

/// Test if email/password signup is enabled (without creating a real account).
pub fn test_email_signup_capability(
    client: &Client,
    key: &str,
    info: &mut FirebaseProjectInfo,
) -> TestResult {
    let url = format!(
        "https://identitytoolkit.googleapis.com/v1/accounts:signUp?key={}",
        key
    );
    let body = serde_json::json!({
        "email": "test_capability_check_12345@nonexistent-domain-xyz.invalid",
        "password": "TestPassword123!",
        "returnSecureToken": true
    });

    let resp = match client.post(&url).json(&body).send() {
        Ok(r) => r,
        Err(e) => return TestResult::fail(None, e.to_string(), None),
    };

    let status = resp.status().as_u16();
    let data: Value = resp.json().unwrap_or_default();
    let error_msg = extract_error_message(&data);

    if status == 200 {
        info.email_auth_enabled = Some(true);
        info.signup_disabled = Some(false);
        TestResult::ok(status, "Email signup ENABLED — account creation allowed")
    } else if error_msg.contains("OPERATION_NOT_ALLOWED") {
        info.email_auth_enabled = Some(false);
        TestResult::fail(
            Some(status),
            "OPERATION_NOT_ALLOWED",
            Some("Email/password auth is DISABLED".into()),
        )
    } else if error_msg.contains("ADMIN_ONLY_OPERATION")
        || error_msg.to_uppercase().contains("DISALLOWED")
    {
        info.signup_disabled = Some(true);
        info.email_auth_enabled = Some(true);
        TestResult::fail(
            Some(status),
            &error_msg,
            Some("Email auth enabled but signup RESTRICTED (admin only)".into()),
        )
    } else if error_msg.contains("INVALID_EMAIL") || error_msg.contains("WEAK_PASSWORD") {
        info.email_auth_enabled = Some(true);
        TestResult::ok(
            status,
            "Email/password auth is ENABLED (validation active)",
        )
    } else if error_msg.contains("EMAIL_EXISTS") {
        info.email_auth_enabled = Some(true);
        TestResult::ok(
            status,
            "Email/password auth ENABLED (email collision check works)",
        )
    } else {
        TestResult::fail(Some(status), if error_msg.is_empty() { "Unknown" } else { &error_msg }, None)
    }
}

/// Enumerate auth providers via createAuthUri endpoint.
pub fn test_fetch_providers(
    client: &Client,
    key: &str,
    info: &mut FirebaseProjectInfo,
) -> TestResult {
    let url = format!(
        "https://identitytoolkit.googleapis.com/v1/accounts:createAuthUri?key={}",
        key
    );
    let body = serde_json::json!({
        "identifier": "test@gmail.com",
        "continueUri": "http://localhost"
    });

    let resp = match client.post(&url).json(&body).send() {
        Ok(r) => r,
        Err(e) => return TestResult::fail(None, e.to_string(), None),
    };

    let status = resp.status().as_u16();
    let data: Value = resp.json().unwrap_or_default();

    if status == 200 {
        let mut all_providers: Vec<String> = Vec::new();

        if let Some(arr) = data.get("allProviders").and_then(|v| v.as_array()) {
            for p in arr {
                if let Some(s) = p.as_str() {
                    all_providers.push(s.to_string());
                }
            }
        }
        if let Some(arr) = data.get("signinMethods").and_then(|v| v.as_array()) {
            for p in arr {
                if let Some(s) = p.as_str() {
                    if !all_providers.contains(&s.to_string()) {
                        all_providers.push(s.to_string());
                    }
                }
            }
        }

        info.enabled_providers.extend(all_providers.clone());

        let detail = if all_providers.is_empty() {
            "Providers: None configured".to_string()
        } else {
            format!("Providers: {}", all_providers.join(", "))
        };

        let mut result = TestResult::ok(status, detail);
        result = result.with_extra("providers", all_providers.join(","));
        result
    } else {
        let msg = extract_error_message(&data);
        TestResult::fail(Some(status), msg, None)
    }
}

/// Test accounts:lookup endpoint.
pub fn test_identity_toolkit_lookup(
    client: &Client,
    key: &str,
    _info: &mut FirebaseProjectInfo,
) -> TestResult {
    let url = format!(
        "https://identitytoolkit.googleapis.com/v1/accounts:lookup?key={}",
        key
    );
    let body = serde_json::json!({"idToken": "invalid_test_token"});

    let resp = match client.post(&url).json(&body).send() {
        Ok(r) => r,
        Err(e) => return TestResult::fail(None, e.to_string(), None),
    };

    let status = resp.status().as_u16();
    let data: Value = resp.json().unwrap_or_default();
    let error_msg = extract_error_message(&data);

    if status == 400 && error_msg.contains("INVALID_ID_TOKEN") {
        TestResult::ok(
            status,
            "Identity Toolkit accessible (works with valid token)",
        )
    } else if status == 401 || status == 403 {
        TestResult::fail(
            Some(status),
            &error_msg,
            Some("API key not authorized for Identity Toolkit".into()),
        )
    } else {
        TestResult::fail(Some(status), error_msg, None)
    }
}

/// Test securetoken endpoint — token refresh capability.
pub fn test_secure_token(
    client: &Client,
    key: &str,
    _info: &mut FirebaseProjectInfo,
) -> TestResult {
    let url = format!("https://securetoken.googleapis.com/v1/token?key={}", key);
    let params = [
        ("grantType", "refresh_token"),
        ("refreshToken", "invalid_test_token"),
    ];

    let resp = match client.post(&url).form(&params).send() {
        Ok(r) => r,
        Err(e) => return TestResult::fail(None, e.to_string(), None),
    };

    let status = resp.status().as_u16();
    let data: Value = resp.json().unwrap_or_default();
    let error_msg = extract_error_message(&data);

    if status == 400 {
        if error_msg.contains("INVALID_REFRESH_TOKEN") {
            return TestResult::ok(
                status,
                "SecureToken API accessible (refresh would work with valid token)",
            );
        } else if error_msg.contains("API key not valid") {
            return TestResult::fail(
                Some(status),
                "Invalid API key",
                Some("Key not valid for SecureToken API".into()),
            );
        }
    }

    if status == 401 || status == 403 {
        TestResult::fail(Some(status), &error_msg, None)
    } else {
        TestResult {
            success: status != 401 && status != 403,
            status_code: Some(status),
            error: if status == 401 || status == 403 {
                Some(error_msg.clone())
            } else {
                None
            },
            detail: if !error_msg.is_empty() {
                Some(format!("Response: {}", error_msg))
            } else {
                None
            },
            extra: Default::default(),
        }
    }
}

/// Test if password reset emails can be triggered (email enumeration risk).
pub fn test_password_reset(
    client: &Client,
    key: &str,
    _info: &mut FirebaseProjectInfo,
) -> TestResult {
    let url = format!(
        "https://identitytoolkit.googleapis.com/v1/accounts:sendOobCode?key={}",
        key
    );
    let body = serde_json::json!({
        "requestType": "PASSWORD_RESET",
        "email": "nonexistent_test_12345@example.com"
    });

    let resp = match client.post(&url).json(&body).send() {
        Ok(r) => r,
        Err(e) => return TestResult::fail(None, e.to_string(), None),
    };

    let status = resp.status().as_u16();
    let data: Value = resp.json().unwrap_or_default();
    let error_msg = extract_error_message(&data);

    if status == 200 {
        TestResult::ok(
            status,
            "[WARNING] Password reset emails can be sent — potential spam vector",
        )
    } else if error_msg.contains("EMAIL_NOT_FOUND") {
        TestResult::ok(
            status,
            "[INFO] Email enumeration possible via PASSWORD_RESET",
        )
    } else if status == 401 || status == 403 {
        TestResult::fail(
            Some(status),
            &error_msg,
            Some("Password reset endpoint restricted".into()),
        )
    } else {
        TestResult::fail(Some(status), error_msg, None)
    }
}

// ── Helpers ──────────────────────────────────────────────────────────────────

fn extract_error_message(data: &Value) -> String {
    if let Some(err) = data.get("error") {
        if let Some(msg) = err.get("message").and_then(|m| m.as_str()) {
            return msg.to_string();
        }
        return err.to_string();
    }
    String::new()
}