File: src/Builder.php

Recommend this page to a friend!
  Classes of Scott Arciszewski  >  PHP PASeTo  >  src/Builder.php  >  Download  
File: src/Builder.php
Role: Class source
Content type: text/plain
Description: Class source
Class: PHP PASeTo
Encrypt and decrypt data with PaSeTO protocol
Author: By
Last change:
Date: 8 months ago
Size: 17,392 bytes
 

Contents

Class file image Download
<?php
declare(strict_types=1);
namespace ParagonIE\Paseto;

use ParagonIE\Paseto\Exception\{
    EncodingException,
    InvalidKeyException,
    InvalidPurposeException,
    PasetoException
};
use ParagonIE\Paseto\Keys\{
    AsymmetricSecretKey,
    SymmetricKey
};
use ParagonIE\Paseto\Protocol\Version2;
use ParagonIE\Paseto\Traits\RegisteredClaims;

/**
 * Class Builder
 * @package ParagonIE\Paseto
 */
class Builder
{
    use RegisteredClaims;

    /** @var string $cached */
    protected $cached = '';

    /** @var array<string, string> */
    protected $claims = [];

    /** @var \Closure|null $unitTestEncrypter -- Do not use this. It's for unit testing! */
    protected $unitTestEncrypter;

    /** @var SendingKey|null $key */
    protected $key = null;

    /** @var Purpose|null $purpose */
    protected $purpose;

    /** @var ProtocolInterface $version */
    protected $version;

    /** @var JsonToken $token */
    protected $token;

    /**
     * Builder constructor.
     *
     * @param JsonToken|null $baseToken
     * @param ProtocolInterface|null $protocol
     * @param SendingKey|null $key
     *
     * @throws PasetoException
     */
    public function __construct(
        JsonToken $baseToken = null,
        ProtocolInterface $protocol = null,
        SendingKey $key = null
    ) {
        if (!$baseToken) {
            $baseToken = new JsonToken();
        }
        if (!$protocol) {
            $protocol = new Version2();
        }
        $this->token = $baseToken;
        $this->version = $protocol;
        if ($key) {
            $this->setKey($key);
        }
    }

    /**
     * Get any arbitrary claim.
     *
     * @param string $claim
     * @return mixed
     * @throws PasetoException
     */
    public function get(string $claim)
    {
        return $this->token->get($claim);
    }

    /**
     * @return array
     * @throws PasetoException
     */
    public function getFooterArray(): array
    {
        return $this->token->getFooterArray();
    }

    /**
     * Get a Builder instance configured for local usage.
     * (i.e. shared-key authenticated encryption)
     *
     * @param SymmetricKey $key
     * @param ProtocolInterface|null $version
     * @param JsonToken|null $baseToken
     *
     * @return Builder
     * @throws PasetoException
     */
    public static function getLocal(
        SymmetricKey $key,
        ProtocolInterface $version = null,
        JsonToken $baseToken = null
    ): self {
        if (!$version) {
            $version = new Version2();
        }
        $instance = new static($baseToken);
        $instance->key = $key;
        $instance->version = $version;
        $instance->purpose = Purpose::local();
        return $instance;
    }

    /**
     * Get a Builder instance configured for remote usage.
     * (i.e. public-key digital signatures)
     *
     * @param AsymmetricSecretKey $key
     * @param ProtocolInterface|null $version
     * @param JsonToken|null $baseToken
     *
     * @return Builder
     * @throws PasetoException
     */
    public static function getPublic(
        AsymmetricSecretKey $key,
        ProtocolInterface $version = null,
        JsonToken $baseToken = null
    ): self {
        if (!$version) {
            $version = new Version2();
        }
        $instance = new static($baseToken);
        $instance->key = $key;
        $instance->version = $version;
        $instance->purpose = Purpose::public();
        return $instance;
    }

    /**
     * Get the JsonToken object (not the string)
     *
     * @return JsonToken
     */
    public function getJsonToken(): JsonToken
    {
        return $this->token;
    }

    /**
     * Set a claim to an arbitrary value.
     *
     * @param string $claim
     * @param string $value
     *
     * @return self
     */
    public function set(string $claim, $value): self
    {
        $this->token->set($claim, $value);
        return $this;
    }

    /**
     * Set the 'aud' claim for the token we're building. (Mutable.)
     *
     * @param string $aud
     * @return self
     */
    public function setAudience(string $aud): self
    {
        return $this->set('aud', $aud);
    }

    /**
     * Set the 'exp' claim for the token we're building. (Mutable.)
     *
     * @param \DateTimeInterface|null $time
     * @return self
     */
    public function setExpiration(\DateTimeInterface $time = null): self
    {
        if (!$time) {
            $time = new \DateTime('NOW');
        }
        return $this->set('exp', $time->format(\DateTime::ATOM));
    }

    /**
     * Set the 'iat' claim for the token we're building. (Mutable.)
     *
     * @param \DateTimeInterface|null $time
     * @return self
     */
    public function setIssuedAt(\DateTimeInterface $time = null): self
    {
        if (!$time) {
            $time = new \DateTime('NOW');
        }
        return $this->set('iat', $time->format(\DateTime::ATOM));
    }

    /**
     * Set the 'iss' claim for the token we're building. (Mutable.)
     *
     * @param string $iss
     * @return self
     */
    public function setIssuer(string $iss): self
    {
        return $this->set('iss', $iss);
    }

    /**
     * Set the 'jti' claim for the token we're building. (Mutable.)
     *
     * @param string $id
     * @return self
     */
    public function setJti(string $id): self
    {
        return $this->set('jti', $id);
    }

    /**
     * Set the 'nbf' claim for the token we're building. (Mutable.)
     *
     * @param \DateTimeInterface|null $time
     * @return self
     */
    public function setNotBefore(\DateTimeInterface $time = null): self
    {
        if (!$time) {
            $time = new \DateTime('NOW');
        }
        return $this->set('nbf', $time->format(\DateTime::ATOM));
    }

    /**
     * Set the 'sub' claim for the token we're building. (Mutable.)
     *
     * @param string $sub
     * @return self
     */
    public function setSubject(string $sub): self
    {
        return $this->set('sub', $sub);
    }

    /**
     * Set an array of claims in one go.
     *
     * @param array<string, string> $claims
     * @return self
     */
    public function setClaims(array $claims): self
    {
        $this->cached = '';
        $this->token->setClaims($claims);
        return $this;
    }

    /**
     * Set the footer.
     *
     * @param string $footer
     * @return self
     */
    public function setFooter(string $footer = ''): self
    {
        $this->cached = '';
        $this->token->setFooter($footer);
        return $this;
    }

    /**
     * Set the footer, given an array of data. Converts to JSON.
     *
     * @param array $footer
     * @return self
     * @throws PasetoException
     */
    public function setFooterArray(array $footer = []): self
    {
        $encoded = \json_encode($footer);
        if (!\is_string($encoded)) {
            throw new EncodingException('Could not encode array into JSON');
        }
        return $this->setFooter($encoded);
    }

    /**
     * Set the cryptographic key used to authenticate (and possibly encrypt)
     * the serialized token.
     *
     * @param SendingKey $key
     * @param bool $checkPurpose
     * @return self
     * @throws PasetoException
     */
    public function setKey(SendingKey $key, bool $checkPurpose = false): self
    {
        if ($checkPurpose) {
            if (!isset($this->purpose)) {
                throw new InvalidKeyException('Unknown purpose');
            } elseif (!$this->purpose->isSendingKeyValid($key)) {
                throw new InvalidKeyException(
                    'Invalid key type. Expected ' .
                        $this->purpose->expectedSendingKeyType() .
                        ', got ' .
                        \get_class($key)
                );
            }
            switch ($this->purpose) {
                case Purpose::local():
                    break;
                case Purpose::public():
                    if (!($key->getProtocol() instanceof $this->version)) {
                        throw new InvalidKeyException(
                            'Invalid key type. This key is for ' .
                            $key->getProtocol()::header() .
                            ', not ' .
                            $this->version::header()
                        );
                    }
                    break;
                default:
                    throw new InvalidKeyException('Unknown purpose');
            }
        }

        $this->cached = '';
        $this->key = $key;
        return $this;
    }

    /**
     * Set the purpose for this token. Allowed values:
     * Purpose::local(), Purpose::public().
     *
     * @param Purpose $purpose
     * @param bool $checkKeyType
     * @return self
     * @throws InvalidKeyException
     * @throws InvalidPurposeException
     */
    public function setPurpose(Purpose $purpose, bool $checkKeyType = false): self
    {
        if ($checkKeyType) {
            if (\is_null($this->key)) {
                throw new InvalidKeyException('Key cannot be null');
            }
            $expectedPurpose = Purpose::fromSendingKey($this->key);
            if (!$purpose->equals($expectedPurpose)) {
                throw new InvalidPurposeException(
                    'Invalid purpose. Expected '.$expectedPurpose->rawString()
                    .', got ' . $purpose->rawString()
                );
            }
        }

        $this->cached = '';
        $this->purpose = $purpose;
        return $this;
    }

    /**
     * Pass an existing JsonToken object. Useful for updating an existing token.
     *
     * @param JsonToken $token
     *
     * @return Builder
     */
    public function setJsonToken(JsonToken $token): self
    {
        $this->token = $token;
        return $this;
    }

    /**
     * Specify the version of the protocol to be used.
     *
     * @param ProtocolInterface|null $version
     *
     * @return self
     */
    public function setVersion(ProtocolInterface $version = null): self
    {
        if (!$version) {
            $version = new Version2();
        }
        $this->version = $version;
        return $this;
    }

    /**
     * Get the token as a string.
     *
     * @return string
     * @throws PasetoException
     * @psalm-suppress MixedInferredReturnType
     */
    public function toString(): string
    {
        if (!empty($this->cached)) {
            return $this->cached;
        }
        if (\is_null($this->key)) {
            throw new InvalidKeyException('Key cannot be null');
        }
        if (\is_null($this->purpose)) {
            throw new InvalidPurposeException('Purpose cannot be null');
        }
        // Mutual sanity checks
        $this->setKey($this->key, true);
        $this->setPurpose($this->purpose, true);

        $claims = \json_encode($this->token->getClaims());
        $protocol = $this->version;
        ProtocolCollection::throwIfUnsupported($protocol);
        switch ($this->purpose) {
            case Purpose::local():
                if ($this->key instanceof SymmetricKey) {
                    /**
                     * During unit tests, perform last-minute dependency
                     * injection to swap $protocol for a conjured up version.
                     * This new version can access a protected method on our
                     * actual $protocol, giving unit tests the ability to
                     * manually set a pre-decided nonce.
                     */
                    if (isset($this->unitTestEncrypter)) {
                        /** @var ProtocolInterface */
                        $protocol = ($this->unitTestEncrypter)($protocol);
                    }

                    $this->cached = (string) $protocol::encrypt(
                        $claims,
                        $this->key,
                        $this->token->getFooter()
                    );
                    return $this->cached;
                }
                break;
            case Purpose::public():
                if ($this->key instanceof AsymmetricSecretKey) {
                    try {
                        $this->cached = (string) $protocol::sign(
                            $claims,
                            $this->key,
                            $this->token->getFooter()
                        );
                        return $this->cached;
                    } catch (\Throwable $ex) {
                        throw new PasetoException('Signing failed.', 0, $ex);
                    }
                }
                break;
        }
        throw new PasetoException('Unsupported key/purpose pairing.');
    }

    /**
     * Return a new Builder instance with a changed claim.
     *
     * @param string $claim
     * @param string $value
     * @return self
     */
    public function with(string $claim, $value): self
    {
        $cloned = clone $this;
        $cloned->cached = '';
        $cloned->token = $cloned->token->with($claim, $value);
        return $cloned;
    }

    /**
     * Return a new Builder instance with a changed 'aud' claim.
     *
     * @param string $aud
     * @return self
     */
    public function withAudience(string $aud): self
    {
        return (clone $this)->setAudience($aud);
    }

    /**
     * Return a new Builder instance with an array of changed claims.
     *
     * @param array<string, string> $claims
     * @return self
     */
    public function withClaims(array $claims): self
    {
        return (clone $this)->setClaims($claims);
    }

    /**
     * Return a new Builder instance with a changed 'exp' claim.
     *
     * @param \DateTimeInterface|null $time
     * @return self
     */
    public function withExpiration(\DateTimeInterface $time = null): self
    {
        return (clone $this)->setExpiration($time);
    }

    /**
     * Return a new Builder instance with a changed footer.
     *
     * @param string $footer
     * @return self
     */
    public function withFooter(string $footer = ''): self
    {
        return (clone $this)->setFooter($footer);
    }

    /**
     * Return a new Builder instance with a changed footer,
     * representing the JSON-encoded array provided.
     *
     * @param array $footer
     * @return self
     * @throws PasetoException
     */
    public function withFooterArray(array $footer = []): self
    {
        return (clone $this)->setFooterArray($footer);
    }

    /**
     * Return a new Builder instance with a changed 'iat' claim.
     *
     * @param \DateTimeInterface|null $time
     * @return self
     */
    public function withIssuedAt(\DateTimeInterface $time = null): self
    {
        return (clone $this)->setIssuedAt($time);
    }

    /**
     * Return a new Builder instance with a changed 'iss' claim.
     *
     * @param string $iss
     * @return self
     */
    public function withIssuer(string $iss): self
    {
        return (clone $this)->setIssuer($iss);
    }

    /**
     * Return a new Builder instance with a changed 'jti' claim.
     *
     * @param string $id
     * @return self
     */
    public function withJti(string $id): self
    {
        return (clone $this)->setJti($id);
    }

    /**
     * Return a new Builder instance with a changed 'nbf' claim.
     *
     * @param \DateTimeInterface|null $time
     * @return self
     */
    public function withNotBefore(\DateTimeInterface $time = null): self
    {
        return (clone $this)->setNotBefore($time);
    }

    /**
     * Return a new Builder instance with a changed 'sub' claim.
     *
     * @param string $sub
     * @return self
     */
    public function withSubject(string $sub): self
    {
        return (clone $this)->setSubject($sub);
    }

    /**
     * Return a new Builder instance, with the provided cryptographic key used
     * to authenticate (and possibly encrypt) the serialized token.
     *
     * @param SendingKey $key
     * @param bool $checkPurpose
     * @return self
     * @throws PasetoException
     */
    public function withKey(SendingKey $key, bool $checkPurpose = false): self
    {
        return (clone $this)->setKey($key, $checkPurpose);
    }

    /**
     * Return a new Builder instance with a new purpose.
     * Allowed values:
     * Purpose::local(), Purpose::public().
     *
     * @param Purpose $purpose
     * @param bool $checkKeyType
     * @return self
     * @throws InvalidKeyException
     * @throws InvalidPurposeException
     */
    public function withPurpose(Purpose $purpose, bool $checkKeyType = false): self
    {
        return (clone $this)->setPurpose($purpose, $checkKeyType);
    }

    /**
     * Return a new Builder instance with the specified JsonToken object.
     *
     * @param JsonToken $token
     *
     * @return Builder
     */
    public function withJsonToken(JsonToken $token): self
    {
        return (clone $this)->setJsonToken($token);
    }

    /**
     * Make a copy of the JsonToken object.
     *
     * @return void
     */
    public function __clone()
    {
        $this->token = clone $this->token;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        try {
            return $this->toString();
        } catch (\Throwable $ex) {
            return '';
        }
    }
}

For more information send a message to info at phpclasses dot org.