<?php
Phar::mapPhar('module-guard.phar');

spl_autoload_register(function (string $class): void {
    $prefix = 'Corbital\\ModuleManager\\Security\\';
    if (strpos($class, $prefix) !== 0) {
        return;
    }
    $relative = str_replace('\\', '/', substr($class, strlen($prefix)));
    $path = 'phar://module-guard.phar/Security/' . $relative . '.php';
    if (file_exists($path)) {
        require_once $path;
    }
});

__HALT_COMPILER(); ?>
            module-guard.phar       Security/PaidModuleRegistry.php        R      #   Security/ModuleIntegrityChecker.php	      	  `:'      $   Security/ModuleSignatureVerifier.php<
      <
  	P         Security/LicenseEnforcer.php9      9  ɸyl      <?php

namespace Corbital\ModuleManager\Security;

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

class PaidModuleRegistry
{
    /**
     * Resolve the registry, preferring the remote API with a cache layer.
     */
    private static function getRegistry(): array
    {
        return Cache::remember('paid_module_registry', now()->addHours(6), function () {
            return static::fetchFromApi() ?? [];
        });
    }

    /**
     * Fetch the module registry from the remote API.
     * Returns null on failure so the caller can fall back.
     */
    private static function fetchFromApi(): ?array
    {
        try {
            $encoded = config('installer.license_verification.api_endpoint');
            if (! $encoded) {
                return null;
            }

            $url = rtrim(base64_decode($encoded), '/');
            $response = Http::timeout(60)
                ->withHeaders([
                    'Accept' => 'application/json',
                    'Content-Type' => 'application/json',
                ])
                ->get($url.'/module-registry');

            if (! $response->successful()) {
                return null;
            }

            $data = $response->json('data');
            if (! is_array($data) || empty($data)) {
                return null;
            }

            return $data;
        } catch (\Throwable $e) {
            return null;
        }
    }

    /**
     * Check if a module is registered as a paid module.
     */
    public static function isPaidModule(string $name): bool
    {
        return isset(static::getRegistry()[$name]);
    }

    /**
     * Get the Envato item ID for a paid module.
     */
    public static function getItemId(string $name): ?string
    {
        $moduleJson = static::readModuleJson($name);

        if (is_null($moduleJson)) {
            return null;
        }   

        $module = static::getRegistry()[$name] ?? null;

        if (is_null($module)) {
            return null;
        }

        $itemId = isset($moduleJson['allow_type']) && !empty($module['product_id']) ? $module['product_id'] : (!empty($module['item_id']) ? $module['item_id'] : null);

        return $itemId ?? null;
    }

    /**
     * Get full metadata for a paid module.
     */
    public static function getModuleMeta(string $name): ?array
    {
        return static::getRegistry()[$name] ?? null;
    }

    /**
     * Read a module's module.json file and return its decoded contents.
     */
    public static function readModuleJson(string $name): ?array
    {
        $path = base_path("Modules/{$name}/module.json");

        if (! is_file($path)) {
            return null;
        }

        $raw = @file_get_contents($path);
        if ($raw === false) {
            return null;
        }

        $data = json_decode($raw, true);

        return is_array($data) ? $data : null;
    }

    /**
     * Get all registered paid modules.
     */
    public static function allPaidModules(): array
    {
        return static::getRegistry();
    }

    /**
     * Validate module.json fields against the registry.
     * Returns array of violations, empty if clean.
     */
    public static function validateModuleJson(string $name, array $moduleJson): array
    {
        if (! static::isPaidModule($name)) {
            return [];
        }

        $expected = static::getRegistry()[$name];
        $violations = [];

        // Check type field
        $actualType = $moduleJson['type'] ?? 'addon';
        if ($actualType !== $expected['type']) {
            $violations[] = "type mismatch: expected '{$expected['type']}', found '{$actualType}'";
        }

        // Check license_required field
        $actualLicenseRequired = $moduleJson['license_required'] ?? false;
        if ($actualLicenseRequired !== true) {
            $violations[] = 'license_required is missing or false';
        }

        // Check license_product_id field
        $actualProductId = $moduleJson['license_product_id'] ?? null;

        if (isset($moduleJson['allow_type']) && !empty($expected['product_id'])) {
            if ($actualProductId !== $expected['product_id']) {
                $violations[] = "license_product_id mismatch: expected '{$expected['product_id']}', found '{$actualProductId}'";
            }
        }

        if (!isset($moduleJson['allow_type']) && !empty($expected['item_id'])) {
            if ($actualProductId !== $expected['item_id']) {
                $violations[] = "license_product_id mismatch: expected '{$expected['item_id']}', found '{$actualProductId}'";
            }
        }

        // Check for skip/nulled flags — should never be present on paid modules
        if (! empty($moduleJson['skv'])) {
            $violations[] = 'skv flag found on paid module';
        }

        if (! empty($moduleJson['skn'])) {
            $violations[] = 'skn flag found on paid module';
        }

        return $violations;
    }
}
<?php

namespace Corbital\ModuleManager\Security;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;

class ModuleIntegrityChecker
{
    /**
     * Mark a module as verified after successful activation.
     */
    public static function storeHashes(string $moduleName): void
    {
        DB::table('modules')
            ->where('name', $moduleName)
            ->update([
                'last_integrity_check' => now(),
                'integrity_status' => 'verified',
            ]);
    }

    /**
     * Verify the integrity of a module by validating module.json against the registry.
     * Returns an array of violations, empty if clean.
     */
    public static function verify(string $moduleName): array
    {
        if (! PaidModuleRegistry::isPaidModule($moduleName)) {
            return [];
        }

        $violations = [];
        $modulePath = base_path('Modules/'.$moduleName);
        $moduleJsonPath = $modulePath.'/module.json';

        $moduleRecord = DB::table('modules')
            ->where('name', $moduleName)
            ->first();

        if (! $moduleRecord) {
            return [];
        }

        // Validate module.json content against registry
        if (File::exists($moduleJsonPath)) {
            $moduleJson = json_decode(File::get($moduleJsonPath), true);
            if ($moduleJson) {
                $registryViolations = PaidModuleRegistry::validateModuleJson($moduleName, $moduleJson);
                $violations = array_merge($violations, $registryViolations);
            }
        }

        // Update integrity status in DB
        $status = empty($violations) ? 'verified' : 'tampered';
        DB::table('modules')
            ->where('name', $moduleName)
            ->update([
                'last_integrity_check' => now(),
                'integrity_status' => $status,
            ]);

        return $violations;
    }

    /**
     * Run integrity check on all active paid modules.
     * Returns array of [module_name => violations].
     */
    public static function verifyAllActive(): array
    {
        $results = [];
        $moduleManager = app('module.manager');

        foreach (PaidModuleRegistry::allPaidModules() as $name => $meta) {
            if ($moduleManager->isActive($name)) {
                $violations = static::verify($name);
                if (! empty($violations)) {
                    $results[$name] = $violations;
                }
            }
        }

        return $results;
    }
}
<?php

namespace Corbital\ModuleManager\Security;

class ModuleSignatureVerifier
{
    /**
     * The HMAC algorithm to use.
     */
    private const ALGO = 'sha256';

    /**
     * Fields from module.json that are signed (in order).
     */
    private const SIGNED_FIELDS = ['name', 'type', 'license_required', 'license_product_id', 'version'];

    /**
     * Verify the HMAC signature of a module.json.
     * Returns true if signature is valid, false otherwise.
     */
    public static function verify(array $moduleJson): bool
    {
        $signature = $moduleJson['signature'] ?? null;

        if (empty($signature)) {
            return false;
        }

        $payload = static::buildPayload($moduleJson);
        $key = static::getVerificationKey();
        $expected = hash_hmac(static::ALGO, $payload, $key);

        return hash_equals($expected, $signature);
    }

    /**
     * Sign a module.json array and return the signature.
     * Used by the build process only — NOT shipped in production.
     */
    public static function sign(array $moduleJson): string
    {
        $payload = static::buildPayload($moduleJson);
        $key = static::getVerificationKey();

        return hash_hmac(static::ALGO, $payload, $key);
    }

    /**
     * Check if a paid module has a valid signature.
     * For paid modules (in registry), signature is required.
     * For non-paid modules, signature check is skipped.
     */
    public static function requiresValidSignature(string $moduleName): bool
    {
        return false;
    }

    /**
     * Build the canonical payload string from module.json fields.
     */
    private static function buildPayload(array $moduleJson): string
    {
        $parts = [];
        foreach (static::SIGNED_FIELDS as $field) {
            $value = $moduleJson[$field] ?? '';
            if (is_bool($value)) {
                $value = $value ? '1' : '0';
            }
            $parts[] = $field.'='.$value;
        }

        return implode('|', $parts);
    }

    /**
     * Get the HMAC verification key.
     * The key is assembled from multiple parts to make it harder to extract.
     */
    private static function getVerificationKey(): string
    {
        // Part 1: embedded in this class
        $p1 = base64_decode('Y29yYml0YWxfbW9kdWxlX2d1YXJk');

        // Part 2: derived from the installer config
        $p2 = substr(md5(config('installer.license_verification.product_id', 'default')), 0, 16);

        // Part 3: static salt
        $p3 = hex2bin('6d6f64756c655f7369676e6174757265');

        return hash('sha256', $p1.$p2.$p3);
    }
}
<?php

namespace Corbital\ModuleManager\Security;

use Corbital\ModuleManager\Classes\ModuleInstall;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

class LicenseEnforcer
{
    /**
     * Grace period in days when license server is unreachable.
     */
    private const GRACE_PERIOD_DAYS = 7;

    /**
     * Check if a module is properly licensed and not tampered with.
     * This is the main entry point for all license checks.
     */
    public static function isModuleLicensed(string $moduleName): bool
    {
        // Not a paid module — always allowed
        if (! PaidModuleRegistry::isPaidModule($moduleName)) {
            return true;
        }

        $itemId = PaidModuleRegistry::getItemId($moduleName);

        // Check 1: Is the module locked due to tampering?
        if (static::isModuleLocked($moduleName)) {
            return false;
        }

        // Check 2: Does a valid local license exist?
        $moduleInstall = new ModuleInstall;
        if (! $moduleInstall->requiresInstallation($itemId)) {
            return true; // License marker file exists and is valid
        }

        return false;
    }

    /**
     * Check if a module is in tamper-locked state.
     * Checks local lock file, DB, and server-side lock status.
     */
    public static function isModuleLocked(string $moduleName): bool
    {
        $itemId = PaidModuleRegistry::getItemId($moduleName);
        if (! $itemId) {
            return false;
        }

        // Check local lock file
        $lockFile = static::lockFilePath($itemId);
        if (File::exists($lockFile)) {
            $lockData = json_decode(base64_decode(File::get($lockFile)), true);
            if (! empty($lockData['locked'])) {
                return true;
            }
        }

        // Check DB integrity status
        $module = DB::table('modules')->where('name', $moduleName)->first();
        if ($module && isset($module->integrity_status) && $module->integrity_status === 'locked') {
            return true;
        }

        // Check server-side lock status (cached 1 hour to avoid per-request calls)
        return static::isLockedOnServer($itemId, $moduleName);
    }

    /**
     * Lock a module due to tampering. Requires valid purchase code to unlock.
     */
    public static function lockModule(string $moduleName, string $reason, array $violations = []): void
    {
        $itemId = PaidModuleRegistry::getItemId($moduleName);
        if (! $itemId) {
            return;
        }

        // Record lock on server first to get a signed lock_token
        $lockToken = static::lockOnServer($moduleName, $itemId, $reason, $violations);

        // Write local lock file (includes server-issued token if available)
        $lockData = [
            'locked' => true,
            'module' => $moduleName,
            'item_id' => $itemId,
            'reason' => $reason,
            'violations' => $violations,
            'locked_at' => now()->toIso8601String(),
            'domain' => request()->getHost(),
            'lock_token' => $lockToken,
        ];

        $lockFile = static::lockFilePath($itemId);
        File::put($lockFile, base64_encode(json_encode($lockData, JSON_PRETTY_PRINT)));

        // Update DB
        DB::table('modules')
            ->where('name', $moduleName)
            ->update([
                'integrity_status' => 'locked',
                'active' => false,
            ]);

        // Deactivate the module
        try {
            app('module.manager')->deactivate($moduleName);
        } catch (\Exception $e) {
            // Force deactivation via status file
            static::forceDeactivateInStatusFile($moduleName);
        }

        // Log locally
        static::logTamperEvent($moduleName, $reason, $violations);
    }

    /**
     * Attempt to unlock a module with valid purchase credentials.
     * Calls the dedicated /unlock-module server endpoint which verifies the
     * purchase code AND clears the server-side lock record atomically.
     * Returns true on success, error message string on failure.
     */
    public static function unlockModule(string $moduleName, string $username, string $purchaseCode): bool|string
    {
        $itemId = PaidModuleRegistry::getItemId($moduleName);
        if (! $itemId) {
            return 'Module not found in registry.';
        }

        try {
            $apiEndpoint = rtrim(base64_decode(config('installer.license_verification.api_endpoint')), '/');

            $response = Http::timeout(30)
                ->withHeaders([
                    'Accept' => 'application/json',
                    'Content-Type' => 'application/json',
                ])
                ->post($apiEndpoint.'/unlock-module', [
                    'purchase_code' => $purchaseCode,
                    'username' => $username,
                    'activated_domain' => static::guessUrl(),
                    'item_id' => $itemId,
                    'module_name' => $moduleName,
                ]);

            if (! $response->successful()) {
                $body = $response->json();

                return $body['message'] ?? 'License verification failed.';
            }

            $data = $response->json();
            if (empty($data['success'])) {
                return $data['message'] ?? 'Invalid purchase code or username.';
            }
        } catch (\Exception $e) {
            return 'Could not reach the license server. Please try again later.';
        }

        // Remove local lock file
        $lockFile = static::lockFilePath($itemId);
        if (File::exists($lockFile)) {
            File::delete($lockFile);
        }

        // Bust the cached server lock status so next isModuleLocked() re-checks clean
        Cache::forget('module_lock_status_'.$itemId);

        // Update DB
        DB::table('modules')
            ->where('name', $moduleName)
            ->update([
                'integrity_status' => 'unchecked',
            ]);

        return true;
    }

    /**
     * Run tamper detection for a module.
     * If tampering detected, locks the module automatically.
     * Returns violations array (empty = clean).
     */
    public static function detectTampering(string $moduleName): array
    {
        if (! PaidModuleRegistry::isPaidModule($moduleName)) {
            return [];
        }

        // Validate module.json against registry (covers type, license_required, license_product_id)
        $violations = ModuleIntegrityChecker::verify($moduleName);

        // If violations found, lock the module
        if (! empty($violations)) {
            static::lockModule($moduleName, 'Tamper detection', $violations);
        }

        return $violations;
    }

    /**
     * Run tamper detection on ALL active paid modules.
     */
    public static function scanAllModules(): array
    {
        $results = [];
        $moduleManager = app('module.manager');

        foreach (PaidModuleRegistry::allPaidModules() as $name => $meta) {
            if ($moduleManager->has($name)) {
                $violations = static::detectTampering($name);
                if (! empty($violations)) {
                    $results[$name] = $violations;
                }
            }
        }

        return $results;
    }

    /**
     * Periodic server re-validation for active paid modules.
     * Called by scheduler or middleware.
     */
    public static function periodicRevalidation(): void
    {
        $moduleManager = app('module.manager');

        foreach (PaidModuleRegistry::allPaidModules() as $name => $meta) {
            if (! $moduleManager->has($name) || ! $moduleManager->isActive($name)) {
                continue;
            }

            $module = DB::table('modules')->where('name', $name)->first();
            if (! $module) {
                continue;
            }

            // Check if enough time has passed since last check
            $lastCheck = $module->last_integrity_check ?? null;
            if ($lastCheck && now()->diffInHours($lastCheck) < 24) {
                continue; // Checked within last 24 hours
            }

            // Run tamper detection
            static::detectTampering($name);
        }
    }

    /**
     * Get the lock file path for a module.
     */
    private static function lockFilePath(string $itemId): string
    {
        return base_path(config('installer.storage_path', 'storage').'/.module_lock_'.$itemId);
    }

    /**
     * Force deactivate a module by editing the status file directly.
     */
    private static function forceDeactivateInStatusFile(string $moduleName): void
    {
        $statusFile = base_path('modules_statuses.json');
        if (File::exists($statusFile)) {
            $statuses = json_decode(File::get($statusFile), true) ?? [];
            $statuses[$moduleName] = false;
            File::put($statusFile, json_encode($statuses, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
    }

    /**
     * Check the license server for a server-side lock on this module+domain.
     * Result is cached for 1 hour. If the server is unreachable, local state
     * is trusted (grace period — no false positives due to network issues).
     * If server confirms a lock but no local lock file exists, one is written.
     */
    private static function isLockedOnServer(string $itemId, string $moduleName): bool
    {
        $cacheKey = 'module_lock_status_'.$itemId;

        $cached = Cache::get($cacheKey);
        if ($cached !== null) {
            return (bool) $cached;
        }

        try {
            $apiEndpoint = rtrim(base64_decode(config('installer.license_verification.api_endpoint')), '/');

            $response = Http::timeout(10)
                ->withHeaders([
                    'Accept' => 'application/json',
                    'Content-Type' => 'application/json',
                ])
                ->get($apiEndpoint.'/module-status/'.$itemId, [
                    'domain' => request()->getHost(),
                ]);

            if (! $response->successful()) {
                // Server unreachable — trust local state, do not lock
                return false;
            }

            $data = $response->json();
            $isLocked = ! empty($data['locked']);

            Cache::put($cacheKey, $isLocked, now()->addHour());

            // Server says locked but no local lock file — write one so it survives offline
            if ($isLocked) {
                $lockFile = static::lockFilePath($itemId);
                if (! File::exists($lockFile)) {
                    $lockData = [
                        'locked' => true,
                        'module' => $moduleName,
                        'item_id' => $itemId,
                        'reason' => $data['reason'] ?? 'Server-side lock',
                        'locked_at' => $data['locked_at'] ?? now()->toIso8601String(),
                        'domain' => request()->getHost(),
                        'source' => 'server',
                    ];
                    File::put($lockFile, base64_encode(json_encode($lockData, JSON_PRETTY_PRINT)));

                    DB::table('modules')
                        ->where('name', $moduleName)
                        ->update(['integrity_status' => 'locked', 'active' => false]);
                }
            }

            return $isLocked;
        } catch (\Exception $e) {
            // Server unreachable — trust local state
            return false;
        }
    }

    /**
     * Record a tamper lock on the license server for this domain+module.
     * Returns the server-issued lock_token (stored in the local lock file),
     * or null if the server is unreachable (lock still applies locally).
     * Also invalidates the cached lock status so subsequent checks re-query.
     */
    private static function lockOnServer(string $moduleName, string $itemId, string $reason, array $violations): ?string
    {
        try {
            $apiEndpoint = rtrim(base64_decode(config('installer.license_verification.api_endpoint')), '/');

            $response = Http::timeout(10)
                ->withHeaders([
                    'Accept' => 'application/json',
                    'Content-Type' => 'application/json',
                ])
                ->post($apiEndpoint.'/lock-module', [
                    'module_name' => $moduleName,
                    'item_id' => $itemId,
                    'domain' => static::guessUrl(),
                    'ip' => request()->ip(),
                    'reason' => $reason,
                    'violations' => $violations,
                    'timestamp' => now()->toIso8601String(),
                ]);

            Cache::forget('module_lock_status_'.$itemId);

            if ($response->successful()) {
                return $response->json('lock_token');
            }
        } catch (\Exception $e) {
            // Silent fail — local lock is still written even if server is unreachable
        }

        return null;
    }

    public static function guessUrl(): string
    {
        $guessedUrl = isset($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) == 'on' ? 'https' : 'http';
        $guessedUrl .= '://'.($_SERVER['HTTP_HOST'] ?? 'localhost');

        if (! isset($_SERVER['HERD_SITE_PATH']) && ! isset($_SERVER['HERD_HOME'])) {
            $scriptName = $_SERVER['SCRIPT_NAME'] ?? '';
            if ($scriptName) {
                $guessedUrl .= str_replace(basename($scriptName), '', $scriptName);
            }
        }

        $guessedUrl = preg_replace('/install.*/', '', $guessedUrl);

        return rtrim($guessedUrl, '/');
    }

    /**
     * Log a tamper event to the local database.
     */
    private static function logTamperEvent(string $moduleName, string $reason, array $violations): void
    {
        try {
            DB::table('module_validation_logs')->insert([
                'module_name' => $moduleName,
                'username' => 'TAMPER_LOCK',
                'purchase_code' => 'N/A',
                'ip_address' => request()->ip(),
                'user_agent' => request()->userAgent(),
                'user_id' => auth()->id(),
                'status' => 'failed',
                'validation_response' => json_encode([
                    'event' => 'tamper_lock',
                    'reason' => $reason,
                    'violations' => $violations,
                ]),
                'created_at' => now(),
            ]);
        } catch (\Exception $e) {
            Log::warning("Failed to log tamper event for {$moduleName}: ".$e->getMessage());
        }
    }
}
NnaΆt?"'?3($+w6>_I>gLqJ#a@&[r&-ҷQ&rL,^SFg]t/lJoЧXT!`LBk&ly	ɖ-jL{Cȩ7*m^	mlYBM5 76}6Ԕ~@DpE䊫b`fBGRe'#۔rԵTv'zRB+cف
M ȫ(і| È      GBMB