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 base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use rand::Rng;
use reqwest::blocking::Client;
use serde_json::Value;

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

/// Generate a Firebase Installation ID (FID) — 22-char base64url string.
pub fn generate_fid() -> String {
    let mut rng = rand::thread_rng();
    let mut bytes = [0u8; 17];
    rng.fill(&mut bytes);
    // Set the first 4 bits to 0111 (required by FIS spec)
    bytes[0] = (bytes[0] & 0x0F) | 0x70;
    URL_SAFE_NO_PAD.encode(bytes)
}

/// Test Firebase Installations API — emulates what the app does on first launch.
pub fn test_firebase_installations(
    client: &Client,
    config: &FirebaseConfig,
    info: &mut FirebaseProjectInfo,
) -> TestResult {
    let (app_id, project_id) = match (&config.app_id, &config.project_id) {
        (Some(a), Some(p)) => (a.clone(), p.clone()),
        _ => {
            return TestResult::fail(
                None,
                "Missing app_id or project_id",
                Some("Firebase Installations requires app_id and project_id".into()),
            )
        }
    };

    let url = format!(
        "https://firebaseinstallations.googleapis.com/v1/projects/{}/installations",
        project_id
    );

    let fid = generate_fid();

    let body = serde_json::json!({
        "fid": fid,
        "appId": app_id,
        "authVersion": "FIS_v2",
        "sdkVersion": "a:17.2.0",
    });

    let resp = match client
        .post(&url)
        .header("X-Goog-Api-Key", &config.api_key)
        .header(
            "x-firebase-client",
            "fire-installations/17.2.0 android/34 fire-android/34.0.0",
        )
        .header("x-android-package", "com.flintbase.test")
        .header("x-android-cert", "placeholder")
        .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();

    match status {
        200 => {
            info.installations_api_enabled = Some(true);
            let mut result = TestResult::ok(
                status,
                "Firebase Installations API accessible — device registration works",
            );
            if let Some(f) = data.get("fid").and_then(|v| v.as_str()) {
                result = result.with_extra("fid", f);
            }
            if let Some(rt) = data.get("refreshToken").and_then(|v| v.as_str()) {
                let preview = if rt.len() > 30 {
                    format!("{}...", &rt[..30])
                } else {
                    rt.to_string()
                };
                result = result.with_extra("refresh_token", preview);
            }
            if let Some(at) = data
                .get("authToken")
                .and_then(|v| v.get("token"))
                .and_then(|v| v.as_str())
            {
                let preview = if at.len() > 30 {
                    format!("{}...", &at[..30])
                } else {
                    at.to_string()
                };
                result = result.with_extra("auth_token", preview);
            }
            result
        }
        401 => TestResult::fail(
            Some(status),
            "Unauthorized",
            Some("API key not valid or package/cert mismatch".into()),
        ),
        403 => {
            let msg = data
                .get("error")
                .and_then(|e| e.get("message"))
                .and_then(|m| m.as_str())
                .unwrap_or("Forbidden");
            TestResult::fail(
                Some(status),
                msg,
                Some("Firebase Installations API restricted".into()),
            )
        }
        _ => {
            let msg = data
                .get("error")
                .and_then(|e| e.get("message"))
                .and_then(|m| m.as_str())
                .map(|s| s.to_string())
                .unwrap_or_else(|| format!("HTTP {}", status));
            TestResult::fail(Some(status), msg, None)
        }
    }
}