owncloud/core

View on GitHub
lib/private/Security/SignedUrl/Verifier.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php
/**
 * @author Thomas Müller <thomas.mueller@tmit.eu>
 *
 * @copyright Copyright (c) 2020, ownCloud GmbH
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program.  If not, see <http://www.gnu.org/licenses/>
 *
 */

namespace OC\Security\SignedUrl;

use OCP\IConfig;
use Sabre\HTTP\RequestInterface;

class Verifier {
    /**
     * @var RequestInterface
     */
    private $request;
    /**
     * @var IConfig
     */
    private $config;
    /**
     * @var \DateTime|null
     */
    private $now;

    public function __construct(RequestInterface $request, IConfig $config, \DateTime $now = null) {
        $this->request = $request;
        $this->config = $config;
        $this->now = $now ?? new \DateTime();
    }

    public function isSignedRequest(): bool {
        $params = $this->getQueryParameters();
        return isset($params['OC-Signature']);
    }

    public function signedRequestIsValid(): bool {
        $params = $this->getQueryParameters();
        if (!isset($params['OC-Signature'], $params['OC-Credential'], $params['OC-Date'], $params['OC-Expires'], $params['OC-Verb'])) {
            $q = \json_encode($params);
            \OC::$server->getLogger()->debug("Query parameters are missing: $q", ['app' => 'signed-url']);
            return false;
        }
        $urlSignature = $params['OC-Signature'];
        $urlCredential = $params['OC-Credential'];
        $urlDate = $params['OC-Date'];
        $urlExpires = $params['OC-Expires'];
        $urlVerb = \strtoupper($params['OC-Verb']);
        $algo = $params['OC-Algo'] ?? 'PBKDF2/10000-SHA512';

        unset($params['OC-Signature'], $params['OC-Algo']);

        $valid = $this->verifySignature($params, $urlCredential, $algo, $urlSignature);
        if (!$valid) {
            return false;
        }
        $verb = \strtoupper($this->getMethod());
        if ($verb !== $urlVerb) {
            \OC::$server->getLogger()->debug("OC-Verb does not match: $verb !== $urlVerb", ['app' => 'signed-url']);
            return false;
        }
        $date = new \DateTime($urlDate);
        $date->add(new \DateInterval("PT{$urlExpires}S"));
        if (!($date < $this->now)) {
            return true;
        }
        \OC::$server->getLogger()->debug("Signature expired: {$date->format(\DateTime::ATOM)} < {$this->now->format(\DateTime::ATOM)}", ['app' => 'signed-url']);
        return false;
    }

    private function getQueryParameters(): array {
        return $this->request->getQueryParameters();
    }

    public function getUrlCredential(): string {
        $params = $this->getQueryParameters();
        if (!isset($params['OC-Credential'])) {
            throw new \LogicException('OC-Credential not set');
        }

        return $params['OC-Credential'];
    }

    private function getAbsoluteUrl(): string {
        return $this->request->getAbsoluteUrl();
    }

    private function getMethod(): string {
        return $this->request->getMethod();
    }

    /**
     * @param string $algo
     * @param string $url
     * @param $signingKey
     * @return false|mixed|string
     */
    protected function computeHash(string $algo, string $url, $signingKey) {
        if (\preg_match('/^(.*)\/(.*)-(.*)$/', $algo, $output)) {
            if ($output[1] !== 'PBKDF2') {
                return false;
            }
            if ($output[3] !== 'SHA512') {
                return false;
            }
            $iterations = (int)$output[2];
            if ($iterations <= 0) {
                return false;
            }
            return \hash_pbkdf2("sha512", $url, $signingKey, $iterations, 64, false);
        }
        return false;
    }

    /**
     * @param array $params
     * @param $urlCredential
     * @param $algo
     * @param $urlSignature
     * @return bool
     * @throws \Sabre\Uri\InvalidUriException
     */
    private function verifySignature(array $params, $urlCredential, $algo, $urlSignature): bool {
        $trustedList = $this->config->getSystemValue('trusted_domains', []);
        $signingKey = $this->config->getUserValue($urlCredential, 'core', 'signing-key');
        // in case the signing key is not initialized, no signature can ever be verified
        if ($signingKey === '') {
            \OC::$server->getLogger()->error("No signing key available for the user $urlCredential. Access via pre-signed URL denied.", ['app' => 'signed-url']);
            return false;
        }
        $qp = \preg_replace('/%5B\d+%5D/', '%5B%5D', \http_build_query($params));

        foreach ($trustedList as $trustedDomain) {
            foreach (['https', 'http'] as $scheme) {
                $url = \Sabre\Uri\parse($this->getAbsoluteUrl());
                $url['scheme'] = $scheme;
                $url['host'] = $trustedDomain;
                $url['query'] = $qp;
                $url = \Sabre\Uri\build($url);

                $hash = $this->computeHash($algo, $url, $signingKey);
                if ($hash === $urlSignature) {
                    return true;
                }
                \OC::$server->getLogger()->debug("Hashes do not match: $hash !== $urlSignature (used key: $signingKey url: $url", ['app' => 'signed-url']);
            }
        }

        return false;
    }
}