<?php 
namespace ParagonIE\Chronicle\Handlers; 
 
use GuzzleHttp\Exception\GuzzleException; 
use ParagonIE\Chronicle\{ 
    Chronicle, 
    Exception\AccessDenied, 
    Exception\BaseException, 
    Exception\FilesystemException, 
    Exception\InvalidInstanceException, 
    Exception\TargetNotFound, 
    HandlerInterface, 
    Scheduled 
}; 
use ParagonIE\ConstantTime\Base64UrlSafe; 
use ParagonIE\Sapient\Exception\InvalidMessageException; 
use Psr\Http\Message\{ 
    RequestInterface, 
    ResponseInterface 
}; 
use Slim\Http\Request; 
 
/** 
 * Class Revoke 
 * @package ParagonIE\Chronicle\Handlers 
 */ 
class Revoke implements HandlerInterface 
{ 
    /** 
     * The handler gets invoked by the router. This accepts a Request 
     * and returns a Response. 
     * 
     * @param RequestInterface $request 
     * @param ResponseInterface $response 
     * @param array $args 
     * @return ResponseInterface 
     * 
     * @throws AccessDenied 
     * @throws BaseException 
     * @throws FilesystemException 
     * @throws GuzzleException 
     * @throws InvalidInstanceException 
     * @throws InvalidMessageException 
     * @throws TargetNotFound 
     * @throws \SodiumException 
     */ 
    public function __invoke( 
        RequestInterface $request, 
        ResponseInterface $response, 
        array $args = [] 
    ): ResponseInterface { 
        // Sanity checks: 
        if ($request instanceof Request) { 
            if (!$request->getAttribute('authenticated')) { 
                throw new AccessDenied('Unauthenticated request'); 
            } 
            if (!$request->getAttribute('administrator')) { 
                throw new AccessDenied('Unprivileged request'); 
            } 
        } else { 
            throw new \TypeError('Something unexpected happen when attempting to revoke.'); 
        } 
 
        /* Revoking a public key cannot be replayed. */ 
        try { 
            Chronicle::validateTimestamps($request); 
        } catch (\Throwable $ex) { 
            return Chronicle::errorResponse( 
                $response, 
                $ex->getMessage(), 
                $ex->getCode() 
            ); 
        } 
 
        // Get the parsed POST body: 
        $post = $request->getParsedBody(); 
        if (!\is_array($post)) { 
            return Chronicle::errorResponse($response, 'POST body empty or invalid', 406); 
        } 
        if (empty($post['clientid'])) { 
            return Chronicle::errorResponse($response, 'Error: Client ID expected', 401); 
        } 
        if (empty($post['publickey'])) { 
            return Chronicle::errorResponse($response, 'Error: Public key expected', 401); 
        } 
 
        $db = Chronicle::getDatabase(); 
        $db->beginTransaction(); 
 
        /** @var bool $found */ 
        $found = $db->exists( 
            'SELECT count(id) FROM ' . Chronicle::getTableName('clients') . ' WHERE publicid = ? AND publickey = ?', 
            $post['clientid'], 
            $post['publickey'] 
        ); 
        if (!$found) { 
            return Chronicle::errorResponse( 
                $response, 
                'Error: Client not found. It may have already been deleted.', 
                404 
            ); 
        } 
        /** @var bool $isAdmin */ 
        $isAdmin = $db->cell( 
            'SELECT isAdmin FROM ' . Chronicle::getTableName('clients') . ' WHERE publicid = ? AND publickey = ?', 
            $post['clientid'], 
            $post['publickey'] 
        ); 
        if ($isAdmin) { 
            return Chronicle::errorResponse( 
                $response, 
                'You cannot delete administrators from this API.', 
                403 
            ); 
        } 
 
        $db->delete( 
            Chronicle::getTableName('clients', true), 
            [ 
                'publicid' => $post['clientid'], 
                'publickey' => $post['publickey'], 
                'isAdmin' => false 
            ] 
        ); 
        if ($db->commit()) { 
            // Confirm deletion: 
            $result = [ 
                'deleted' => !$db->exists( 
                    'SELECT count(id) FROM ' . 
                    Chronicle::getTableName('clients') . 
                    ' WHERE publicid = ? AND publickey = ?', 
                    $post['clientid'], 
                    $post['publickey'] 
                ) 
            ]; 
 
            if (!$result['deleted']) { 
                $result['reason'] = 'Delete operation was unsuccessful due to unknown reasons.'; 
            } 
            try { 
                $now = (new \DateTime())->format(\DateTime::ATOM); 
            } catch (\Exception $ex) { 
                return Chronicle::errorResponse($response, $ex->getMessage(), 500); 
            } 
 
            $settings = Chronicle::getSettings(); 
            if (!empty($settings['publish-revoked-clients'])) { 
                $serverKey = Chronicle::getSigningKey(); 
                $message = \json_encode( 
                    [ 
                        'server-action' => 'Client Access Revocation', 
                        'now' => $now, 
                        'clientid' => $post['clientid'], 
                        'publickey' => $post['publickey'] 
                    ], 
                    JSON_PRETTY_PRINT 
                ); 
                if (!\is_string($message)) { 
                    throw new \TypeError('Invalid messsage'); 
                } 
                $signature = Base64UrlSafe::encode( 
                    \ParagonIE_Sodium_Compat::crypto_sign_detached( 
                        $message, 
                        $serverKey->getString(true) 
                    ) 
                ); 
                $result['revoke'] = Chronicle::extendBlakechain( 
                    $signature, 
                    $message, 
                    $serverKey->getPublicKey() 
                ); 
 
                // If we need to do a cross-sign, do it now: 
                (new Scheduled())->doCrossSigns(); 
            } 
        } else { 
            /* PDO should have already thrown an exception. */ 
            $db->rollBack(); 
            /** @var array<int, string> $errorInfo */ 
            $errorInfo = $db->errorInfo(); 
            return Chronicle::errorResponse( 
                $response, 
                $errorInfo[0], 
                500 
            ); 
        } 
 
        return Chronicle::getSapient()->createSignedJsonResponse( 
            200, 
            [ 
                'version' => Chronicle::VERSION, 
                'datetime' => (new \DateTime())->format(\DateTime::ATOM), 
                'status' => 'OK', 
                'results' => $result 
            ], 
            Chronicle::getSigningKey(), 
            $response->getHeaders(), 
            $response->getProtocolVersion() 
        ); 
    } 
} 
 
 |