<?php

declare (strict_types=1);
namespace GuardDog\Vendor\Webauthn\AttestationStatement;

use GuardDog\Vendor\Jose\Component\Core\Algorithm as AlgorithmInterface;
use GuardDog\Vendor\Jose\Component\Core\AlgorithmManager;
use GuardDog\Vendor\Jose\Component\KeyManagement\JWKFactory;
use GuardDog\Vendor\Jose\Component\Signature\Algorithm\EdDSA;
use GuardDog\Vendor\Jose\Component\Signature\Algorithm\ES256;
use GuardDog\Vendor\Jose\Component\Signature\Algorithm\ES384;
use GuardDog\Vendor\Jose\Component\Signature\Algorithm\ES512;
use GuardDog\Vendor\Jose\Component\Signature\Algorithm\PS256;
use GuardDog\Vendor\Jose\Component\Signature\Algorithm\PS384;
use GuardDog\Vendor\Jose\Component\Signature\Algorithm\PS512;
use GuardDog\Vendor\Jose\Component\Signature\Algorithm\RS256;
use GuardDog\Vendor\Jose\Component\Signature\Algorithm\RS384;
use GuardDog\Vendor\Jose\Component\Signature\Algorithm\RS512;
use GuardDog\Vendor\Jose\Component\Signature\JWS;
use GuardDog\Vendor\Jose\Component\Signature\JWSVerifier;
use GuardDog\Vendor\Jose\Component\Signature\Serializer\CompactSerializer;
use GuardDog\Vendor\Psr\Clock\ClockInterface;
use GuardDog\Vendor\Psr\EventDispatcher\EventDispatcherInterface;
use GuardDog\Vendor\Psr\Http\Client\ClientInterface;
use GuardDog\Vendor\Psr\Http\Message\RequestFactoryInterface;
use GuardDog\Vendor\Psr\Http\Message\ResponseInterface;
use GuardDog\Vendor\Symfony\Contracts\HttpClient\HttpClientInterface;
use GuardDog\Vendor\Webauthn\AuthenticatorData;
use GuardDog\Vendor\Webauthn\Event\AttestationStatementLoaded;
use GuardDog\Vendor\Webauthn\Event\CanDispatchEvents;
use GuardDog\Vendor\Webauthn\Event\NullEventDispatcher;
use GuardDog\Vendor\Webauthn\Exception\AttestationStatementLoadingException;
use GuardDog\Vendor\Webauthn\Exception\AttestationStatementVerificationException;
use GuardDog\Vendor\Webauthn\Exception\InvalidAttestationStatementException;
use GuardDog\Vendor\Webauthn\Exception\UnsupportedFeatureException;
use GuardDog\Vendor\Webauthn\MetadataService\CertificateChain\CertificateToolbox;
use GuardDog\Vendor\Webauthn\TrustPath\CertificateTrustPath;
use function array_key_exists;
use function count;
use function is_array;
use function is_int;
use function is_string;
use function sprintf;
use const JSON_THROW_ON_ERROR;
/**
 * @deprecated since 4.9.0 and will be removed in 5.0.0. Android SafetyNet is now deprecated.
 */
final class AndroidSafetyNetAttestationStatementSupport implements AttestationStatementSupport, CanDispatchEvents
{
    private ?string $apiKey = null;
    private null|ClientInterface|HttpClientInterface $client = null;
    private readonly CompactSerializer $jwsSerializer;
    private ?JWSVerifier $jwsVerifier = null;
    private ?RequestFactoryInterface $requestFactory = null;
    private int $leeway = 0;
    private int $maxAge = 60000;
    private EventDispatcherInterface $dispatcher;
    public function __construct(private readonly null|ClockInterface $clock = null)
    {
        if ($this->clock === null) {
            trigger_deprecation('web-auth/webauthn-lib', '4.8.0', 'The parameter "$clock" will be required in 5.0.0. Please set a clock instance.');
        }
        if (!class_exists(RS256::class) || !class_exists(JWKFactory::class)) {
            throw UnsupportedFeatureException::create('The algorithm RS256 is missing. Did you forget to install the package web-token/jwt-library?');
        }
        $this->jwsSerializer = new CompactSerializer();
        $this->initJwsVerifier();
        $this->dispatcher = new NullEventDispatcher();
    }
    public function setEventDispatcher(EventDispatcherInterface $eventDispatcher): void
    {
        $this->dispatcher = $eventDispatcher;
    }
    public static function create(null|ClockInterface $clock = null): self
    {
        return new self($clock);
    }
    public function enableApiVerification(ClientInterface|HttpClientInterface $client, string $apiKey, ?RequestFactoryInterface $requestFactory = null): self
    {
        $this->apiKey = $apiKey;
        $this->client = $client;
        $this->requestFactory = $requestFactory;
        if ($requestFactory !== null && !$client instanceof HttpClientInterface) {
            trigger_deprecation('web-auth/metadata-service', '4.7.0', 'The parameter "$requestFactory" will be removed in 5.0.0. Please set it to null and set an Symfony\Contracts\HttpClient\HttpClientInterface as "$client" argument.');
        }
        return $this;
    }
    public function setMaxAge(int $maxAge): self
    {
        $this->maxAge = $maxAge;
        return $this;
    }
    public function setLeeway(int $leeway): self
    {
        $this->leeway = $leeway;
        return $this;
    }
    public function name(): string
    {
        return 'android-safetynet';
    }
    /**
     * @param array<string, mixed> $attestation
     */
    public function load(array $attestation): AttestationStatement
    {
        array_key_exists('attStmt', $attestation) || throw AttestationStatementLoadingException::create($attestation);
        foreach (['ver', 'response'] as $key) {
            array_key_exists($key, $attestation['attStmt']) || throw AttestationStatementLoadingException::create($attestation, sprintf('The attestation statement value "%s" is missing.', $key));
            $attestation['attStmt'][$key] !== '' || throw AttestationStatementLoadingException::create($attestation, sprintf('The attestation statement value "%s" is empty.', $key));
        }
        $jws = $this->jwsSerializer->unserialize($attestation['attStmt']['response']);
        $jwsHeader = $jws->getSignature(0)->getProtectedHeader();
        array_key_exists('x5c', $jwsHeader) || throw AttestationStatementLoadingException::create($attestation, 'The response in the attestation statement must contain a "x5c" header.');
        (is_countable($jwsHeader['x5c']) ? count($jwsHeader['x5c']) : 0) > 0 || throw AttestationStatementLoadingException::create($attestation, 'The "x5c" parameter in the attestation statement response must contain at least one certificate.');
        $certificates = $this->convertCertificatesToPem($jwsHeader['x5c']);
        $attestation['attStmt']['jws'] = $jws;
        $attestationStatement = AttestationStatement::createBasic($this->name(), $attestation['attStmt'], CertificateTrustPath::create($certificates));
        $this->dispatcher->dispatch(AttestationStatementLoaded::create($attestationStatement));
        return $attestationStatement;
    }
    public function isValid(string $clientDataJSONHash, AttestationStatement $attestationStatement, AuthenticatorData $authenticatorData): bool
    {
        $trustPath = $attestationStatement->trustPath;
        $trustPath instanceof CertificateTrustPath || throw InvalidAttestationStatementException::create($attestationStatement, 'Invalid trust path');
        $certificates = $trustPath->certificates;
        $firstCertificate = current($certificates);
        is_string($firstCertificate) || throw InvalidAttestationStatementException::create($attestationStatement, 'No certificate');
        $parsedCertificate = openssl_x509_parse($firstCertificate);
        is_array($parsedCertificate) || throw InvalidAttestationStatementException::create($attestationStatement, 'Invalid attestation object');
        array_key_exists('subject', $parsedCertificate) || throw InvalidAttestationStatementException::create($attestationStatement, 'Invalid attestation object');
        array_key_exists('CN', $parsedCertificate['subject']) || throw InvalidAttestationStatementException::create($attestationStatement, 'Invalid attestation object');
        $parsedCertificate['subject']['CN'] === 'attest.android.com' || throw InvalidAttestationStatementException::create($attestationStatement, 'Invalid attestation object');
        /** @var JWS $jws */
        $jws = $attestationStatement->get('jws');
        $payload = $jws->getPayload();
        $this->validatePayload($payload, $clientDataJSONHash, $authenticatorData);
        //Check the signature
        $this->validateSignature($jws, $trustPath);
        //Check against Google service
        $this->validateUsingGoogleApi($attestationStatement);
        return \true;
    }
    private function validatePayload(?string $payload, string $clientDataJSONHash, AuthenticatorData $authenticatorData): void
    {
        $payload !== null || throw AttestationStatementVerificationException::create('Invalid attestation object');
        $payload = json_decode($payload, \true, flags: JSON_THROW_ON_ERROR);
        array_key_exists('nonce', $payload) || throw AttestationStatementVerificationException::create('Invalid attestation object. "nonce" is missing.');
        $payload['nonce'] === base64_encode(hash('sha256', $authenticatorData->authData . $clientDataJSONHash, \true)) || throw AttestationStatementVerificationException::create('Invalid attestation object. Invalid nonce');
        array_key_exists('ctsProfileMatch', $payload) || throw AttestationStatementVerificationException::create('Invalid attestation object. "ctsProfileMatch" is missing.');
        $payload['ctsProfileMatch'] || throw AttestationStatementVerificationException::create('Invalid attestation object. "ctsProfileMatch" value is false.');
        array_key_exists('timestampMs', $payload) || throw AttestationStatementVerificationException::create('Invalid attestation object. Timestamp is missing.');
        is_int($payload['timestampMs']) || throw AttestationStatementVerificationException::create('Invalid attestation object. Timestamp shall be an integer.');
        $currentTime = ($this->clock?->now()->getTimestamp() ?? time()) * 1000;
        $payload['timestampMs'] <= $currentTime + $this->leeway || throw AttestationStatementVerificationException::create(sprintf('Invalid attestation object. Issued in the future. Current time: %d. Response time: %d', $currentTime, $payload['timestampMs']));
        $currentTime - $payload['timestampMs'] <= $this->maxAge || throw AttestationStatementVerificationException::create(sprintf('Invalid attestation object. Too old. Current time: %d. Response time: %d', $currentTime, $payload['timestampMs']));
    }
    private function validateSignature(JWS $jws, CertificateTrustPath $trustPath): void
    {
        $jwk = JWKFactory::createFromCertificate($trustPath->certificates[0]);
        $isValid = $this->jwsVerifier?->verifyWithKey($jws, $jwk, 0);
        $isValid === \true || throw AttestationStatementVerificationException::create('Invalid response signature');
    }
    private function validateUsingGoogleApi(AttestationStatement $attestationStatement): void
    {
        if ($this->client === null || $this->apiKey === null) {
            return;
        }
        $uri = sprintf('https://www.googleapis.com/androidcheck/v1/attestations/verify?key=%s', urlencode($this->apiKey));
        $requestBody = sprintf('{"signedAttestation":"%s"}', $attestationStatement->get('response'));
        if ($this->client instanceof HttpClientInterface) {
            $responseBody = $this->validateUsingGoogleApiWithSymfonyClient($requestBody, $uri);
        } else {
            $responseBody = $this->validateUsingGoogleApiWithPsrClient($requestBody, $uri);
        }
        $responseBodyJson = json_decode($responseBody, \true, flags: JSON_THROW_ON_ERROR);
        array_key_exists('isValidSignature', $responseBodyJson) || throw AttestationStatementVerificationException::create('Invalid response.');
        $responseBodyJson['isValidSignature'] === \true || throw AttestationStatementVerificationException::create('Invalid response.');
    }
    private function getResponseBody(ResponseInterface $response): string
    {
        $responseBody = '';
        $response->getBody()->rewind();
        do {
            $tmp = $response->getBody()->read(1024);
            if ($tmp === '') {
                break;
            }
            $responseBody .= $tmp;
        } while (\true);
        return $responseBody;
    }
    /**
     * @param string[] $certificates
     *
     * @return string[]
     */
    private function convertCertificatesToPem(array $certificates): array
    {
        foreach ($certificates as $k => $v) {
            $certificates[$k] = CertificateToolbox::fixPEMStructure($v);
        }
        return $certificates;
    }
    private function initJwsVerifier(): void
    {
        $algorithmClasses = [RS256::class, RS384::class, RS512::class, PS256::class, PS384::class, PS512::class, ES256::class, ES384::class, ES512::class, EdDSA::class];
        /** @var AlgorithmInterface[] $algorithms */
        $algorithms = [];
        foreach ($algorithmClasses as $algorithm) {
            if (class_exists($algorithm)) {
                $algorithms[] = new $algorithm();
            }
        }
        $algorithmManager = new AlgorithmManager($algorithms);
        $this->jwsVerifier = new JWSVerifier($algorithmManager);
    }
    private function validateUsingGoogleApiWithSymfonyClient(string $requestBody, string $uri): string
    {
        $response = $this->client->request('POST', $uri, ['headers' => ['content-type' => 'application/json'], 'body' => $requestBody]);
        $response->getStatusCode() === 200 || throw AttestationStatementVerificationException::create('Request did not succeeded');
        return $response->getContent();
    }
    private function validateUsingGoogleApiWithPsrClient(string $requestBody, string $uri): string
    {
        $request = $this->requestFactory->createRequest('POST', $uri);
        $request = $request->withHeader('content-type', 'application/json');
        $request->getBody()->write($requestBody);
        $response = $this->client->sendRequest($request);
        $response->getStatusCode() === 200 || throw AttestationStatementVerificationException::create('Request did not succeeded');
        return $this->getResponseBody($response);
    }
}
