make(Kernel::class)->bootstrap(); $identity = portalQaAssertTarget($target); // Before any write or session invalidation. $directory = $root.'/storage/app/private/investmoz-v2/portal-audit'; $original = DB::table('users')->where('id', 13)->first(); $originalHash = hash('sha256', json_encode($original, JSON_THROW_ON_ERROR)); $mode = $argv[1] ?? 'diagnose'; if ($mode === 'diagnose') { $env = Dotenv\Dotenv::parse(file_get_contents($root.'/.env')); $sourceKey = $env['APP_KEY'] ?? ''; $sourceKey = str_starts_with($sourceKey, 'base64:') ? base64_decode(substr($sourceKey, 7)) : $sourceKey; $checks = []; foreach (['two_factor_secret', 'two_factor_recovery_codes'] as $field) { foreach (['qa' => app('encrypter'), 'source_env' => new Encrypter($sourceKey, config('app.cipher'))] as $label => $encrypter) { try { $value = $encrypter->decrypt($original->$field); $checks[$field][$label] = is_string($value); unset($value); } catch (DecryptException $exception) { $checks[$field][$label] = false; } } } $proof = ['target' => $identity, 'account_id' => 13, 'synthetic_account' => str_ends_with($original->email, '@example.invalid'), 'encrypted_field_checks' => $checks, 'qa_key_fingerprint' => hash('sha256', app('encrypter')->getKey()), 'source_key_differs' => ! hash_equals($sourceKey, app('encrypter')->getKey()), 'config_cache_loaded' => $app->configurationIsCached(), 'active_config_cache_touched' => false, 'source_env_file_sha256' => hash_file('sha256', $root.'/.env'), 'cache_store' => config('cache.default'), 'session_driver_web' => 'file', 'account_13_row_sha256' => $originalHash, 'active_writes' => 0, 'captured_at' => gmdate('c')]; unset($env, $sourceKey); file_put_contents($directory.'/mfa-key-diagnosis-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($proof, JSON_THROW_ON_ERROR).PHP_EOL; } elseif ($mode === 'invalidate') { $sessions = realpath($root.'/storage/app/private/investmoz-v2/qa-opportunity-resources-sessions'); if (! $sessions || ! str_starts_with($sessions, realpath($root.'/storage/app/private').DIRECTORY_SEPARATOR)) { throw new RuntimeException('Private QA session root required.'); } $removed = 0; foreach (new DirectoryIterator($sessions) as $file) { if (! $file->isFile() || $file->isLink() || ! preg_match('/^[a-zA-Z0-9]{40}$/D', $file->getFilename())) { continue; } $data = json_decode(file_get_contents($file->getPathname()), true, 512, JSON_THROW_ON_ERROR); $belongs = is_array($data) && (int) ($data['login']['id'] ?? $data['login.id'] ?? 0) === 13; foreach (is_array($data) ? $data : [] as $key => $value) { $belongs = $belongs || (str_starts_with((string) $key, 'login_web_') && (int) $value === 13); } if ($belongs) { $path = realpath($file->getPathname()); if (! $path || ! str_starts_with($path, $sessions.DIRECTORY_SEPARATOR)) { throw new RuntimeException('Session path outside QA refused.'); } if (! unlink($path)) { throw new RuntimeException('QA session invalidation failed.'); } $removed++; } } $proof = ['target' => $identity, 'account_id' => 13, 'qa_sessions_invalidated' => $removed, 'account_row_unchanged' => hash_equals($originalHash, hash('sha256', json_encode(DB::table('users')->where('id', 13)->first(), JSON_THROW_ON_ERROR))), 'session_ids_exported' => false, 'active_writes' => 0, 'captured_at' => gmdate('c')]; file_put_contents($directory.'/mfa-session-invalidation-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($proof).PHP_EOL; } elseif ($mode === 'prepare') { $file = $directory.'/investor-mfa-qa-private.json'; $credentials = is_file($file) ? json_decode(file_get_contents($file), true, 512, JSON_THROW_ON_ERROR) : ['email' => 'investor-mfa-qa@example.invalid', 'password' => 'QA-'.bin2hex(random_bytes(24))]; $actor = User::firstOrCreate(['email' => $credentials['email']], ['name' => 'Synthetic investor MFA QA', 'password' => Hash::make($credentials['password'])]); $actor->forceFill(['email_verified_at' => $actor->email_verified_at ?? now()])->save(); $credentials['actor_id'] = $actor->id; file_put_contents($file, json_encode($credentials, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); chmod($file, 0600); $negativeFile = $directory.'/mfa-negative-qa-private.json'; $negative = is_file($negativeFile) ? json_decode(file_get_contents($negativeFile), true, 512, JSON_THROW_ON_ERROR) : ['email' => 'mfa-negative-qa@example.invalid', 'password' => 'QA-'.bin2hex(random_bytes(24))]; $failedActor = User::firstOrCreate(['email' => $negative['email']], ['name' => 'Synthetic wrong-key MFA negative control QA', 'password' => Hash::make($negative['password'])]); if ($failedActor->two_factor_secret === null) { $foreign = new Encrypter(random_bytes(32), 'AES-256-CBC'); $failedActor->forceFill(['email_verified_at' => now(), 'two_factor_secret' => $foreign->encrypt('SYNTHETICQAONLY'), 'two_factor_recovery_codes' => $foreign->encrypt('["synthetic-only"]'), 'two_factor_confirmed_at' => now()])->save(); } $negative['actor_id'] = $failedActor->id; file_put_contents($negativeFile, json_encode($negative, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); chmod($negativeFile, 0600); echo json_encode(['target' => $identity, 'actor_id' => $actor->id, 'synthetic' => true, 'mfa_enrollment' => 'must use normal authenticated Fortify endpoints', 'negative_control_actor_id' => $failedActor->id, 'original_account_13_preserved' => hash_equals($originalHash, hash('sha256', json_encode(DB::table('users')->where('id', 13)->first(), JSON_THROW_ON_ERROR))), 'active_writes' => 0]).PHP_EOL; } elseif ($mode === 'verify') { $diagnosis = json_decode(file_get_contents($directory.'/mfa-key-diagnosis-qa.json'), true, 512, JSON_THROW_ON_ERROR); $credentials = json_decode(file_get_contents($directory.'/investor-mfa-qa-private.json'), true, 512, JSON_THROW_ON_ERROR); $actor = User::findOrFail($credentials['actor_id']); $secret = app('encrypter')->decrypt($actor->two_factor_secret); $codes = json_decode(app('encrypter')->decrypt($actor->two_factor_recovery_codes), true, 512, JSON_THROW_ON_ERROR); $proof = ['target' => $identity, 'actor_id' => $actor->id, 'mfa_confirmed' => $actor->two_factor_confirmed_at !== null, 'normal_web_secret_decrypts_in_cli' => hash_equals($credentials['totp_secret'], $secret), 'recovery_code_count' => count($codes), 'key_fingerprint_unchanged' => hash_equals($diagnosis['qa_key_fingerprint'], hash('sha256', app('encrypter')->getKey())), 'source_env_unchanged' => hash_equals($diagnosis['source_env_file_sha256'], hash_file('sha256', $root.'/.env')), 'original_account_13_unchanged' => hash_equals($diagnosis['account_13_row_sha256'], $originalHash), 'key_generated_or_rotated' => false, 'mfa_disabled' => false, 'active_writes' => 0, 'captured_at' => gmdate('c')]; unset($secret, $codes, $credentials); if (! $proof['mfa_confirmed'] || ! $proof['normal_web_secret_decrypts_in_cli'] || ! $proof['key_fingerprint_unchanged'] || ! $proof['source_env_unchanged'] || ! $proof['original_account_13_unchanged'] || $proof['recovery_code_count'] !== 8) { throw new RuntimeException('QA MFA persistence/config/preservation failed.'); } file_put_contents($directory.'/mfa-persistence-qa.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($proof).PHP_EOL; } else { throw new RuntimeException('Unknown QA operation.'); }