<?php

declare (strict_types=1);
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2014-2021 Spomky-Labs
 *
 * This software may be modified and distributed under the terms
 * of the MIT license.  See the LICENSE file for details.
 */
namespace RSSProVendor\Webauthn;

use function array_key_exists;
use RSSProVendor\Assert\Assertion;
use RSSProVendor\Base64Url\Base64Url;
use RSSProVendor\CBOR\Decoder;
use RSSProVendor\CBOR\MapObject;
use RSSProVendor\CBOR\OtherObject\OtherObjectManager;
use RSSProVendor\CBOR\Tag\TagObjectManager;
use InvalidArgumentException;
use function ord;
use RSSProVendor\Psr\Log\LoggerInterface;
use RSSProVendor\Psr\Log\NullLogger;
use RSSProVendor\Ramsey\Uuid\Uuid;
use function RSSProVendor\Safe\json_decode;
use function RSSProVendor\Safe\sprintf;
use function RSSProVendor\Safe\unpack;
use Throwable;
use RSSProVendor\Webauthn\AttestationStatement\AttestationObjectLoader;
use RSSProVendor\Webauthn\AuthenticationExtensions\AuthenticationExtensionsClientOutputsLoader;
class PublicKeyCredentialLoader
{
    private const FLAG_AT = 0b1000000;
    private const FLAG_ED = 0b10000000;
    /**
     * @var AttestationObjectLoader
     */
    private $attestationObjectLoader;
    /**
     * @var Decoder
     */
    private $decoder;
    /**
     * @var LoggerInterface
     */
    private $logger;
    public function __construct(AttestationObjectLoader $attestationObjectLoader, ?LoggerInterface $logger = null)
    {
        if (null !== $logger) {
            @trigger_error('The argument "logger" is deprecated since version 3.3 and will be removed in 4.0. Please use the method "setLogger".', \E_USER_DEPRECATED);
        }
        $this->decoder = new Decoder(new TagObjectManager(), new OtherObjectManager());
        $this->attestationObjectLoader = $attestationObjectLoader;
        $this->logger = $logger ?? new NullLogger();
    }
    public static function create(AttestationObjectLoader $attestationObjectLoader): self
    {
        return new self($attestationObjectLoader);
    }
    public function setLogger(LoggerInterface $logger): self
    {
        $this->logger = $logger;
        return $this;
    }
    /**
     * @param mixed[] $json
     */
    public function loadArray(array $json): PublicKeyCredential
    {
        $this->logger->info('Trying to load data from an array', ['data' => $json]);
        try {
            foreach (['id', 'rawId', 'type'] as $key) {
                Assertion::keyExists($json, $key, sprintf('The parameter "%s" is missing', $key));
                Assertion::string($json[$key], sprintf('The parameter "%s" shall be a string', $key));
            }
            Assertion::keyExists($json, 'response', 'The parameter "response" is missing');
            Assertion::isArray($json['response'], 'The parameter "response" shall be an array');
            Assertion::eq($json['type'], 'public-key', sprintf('Unsupported type "%s"', $json['type']));
            $id = Base64Url::decode($json['id']);
            $rawId = Base64Url::decode($json['rawId']);
            Assertion::true(hash_equals($id, $rawId));
            $publicKeyCredential = new PublicKeyCredential($json['id'], $json['type'], $rawId, $this->createResponse($json['response']));
            $this->logger->info('The data has been loaded');
            $this->logger->debug('Public Key Credential', ['publicKeyCredential' => $publicKeyCredential]);
            return $publicKeyCredential;
        } catch (Throwable $throwable) {
            $this->logger->error('An error occurred', ['exception' => $throwable]);
            throw $throwable;
        }
    }
    public function load(string $data): PublicKeyCredential
    {
        $this->logger->info('Trying to load data from a string', ['data' => $data]);
        try {
            $json = json_decode($data, \true);
            return $this->loadArray($json);
        } catch (Throwable $throwable) {
            $this->logger->error('An error occurred', ['exception' => $throwable]);
            throw $throwable;
        }
    }
    /**
     * @param mixed[] $response
     */
    private function createResponse(array $response): AuthenticatorResponse
    {
        Assertion::keyExists($response, 'clientDataJSON', 'Invalid data. The parameter "clientDataJSON" is missing');
        Assertion::string($response['clientDataJSON'], 'Invalid data. The parameter "clientDataJSON" is invalid');
        switch (\true) {
            case array_key_exists('attestationObject', $response):
                Assertion::string($response['attestationObject'], 'Invalid data. The parameter "attestationObject   " is invalid');
                $attestationObject = $this->attestationObjectLoader->load($response['attestationObject']);
                return new AuthenticatorAttestationResponse(CollectedClientData::createFormJson($response['clientDataJSON']), $attestationObject);
            case array_key_exists('authenticatorData', $response) && array_key_exists('signature', $response):
                $authData = Base64Url::decode($response['authenticatorData']);
                $authDataStream = new StringStream($authData);
                $rp_id_hash = $authDataStream->read(32);
                $flags = $authDataStream->read(1);
                $signCount = $authDataStream->read(4);
                $signCount = unpack('N', $signCount)[1];
                $attestedCredentialData = null;
                if (0 !== (ord($flags) & self::FLAG_AT)) {
                    $aaguid = Uuid::fromBytes($authDataStream->read(16));
                    $credentialLength = $authDataStream->read(2);
                    $credentialLength = unpack('n', $credentialLength)[1];
                    $credentialId = $authDataStream->read($credentialLength);
                    $credentialPublicKey = $this->decoder->decode($authDataStream);
                    Assertion::isInstanceOf($credentialPublicKey, MapObject::class, 'The data does not contain a valid credential public key.');
                    $attestedCredentialData = new AttestedCredentialData($aaguid, $credentialId, (string) $credentialPublicKey);
                }
                $extension = null;
                if (0 !== (ord($flags) & self::FLAG_ED)) {
                    $extension = $this->decoder->decode($authDataStream);
                    $extension = AuthenticationExtensionsClientOutputsLoader::load($extension);
                }
                Assertion::true($authDataStream->isEOF(), 'Invalid authentication data. Presence of extra bytes.');
                $authDataStream->close();
                $authenticatorData = new AuthenticatorData($authData, $rp_id_hash, $flags, $signCount, $attestedCredentialData, $extension);
                return new AuthenticatorAssertionResponse(CollectedClientData::createFormJson($response['clientDataJSON']), $authenticatorData, Base64Url::decode($response['signature']), $response['userHandle'] ?? null);
            default:
                throw new InvalidArgumentException('Unable to create the response object');
        }
    }
}
