From 6ea3fb08ed995b7b734dfd7af532a13346ba15b7 Mon Sep 17 00:00:00 2001 From: David Christofas Date: Wed, 3 Feb 2021 15:21:04 +0100 Subject: [PATCH] pre-signed download urls for password protected public links To support clients which don't use cookies I implemented pre-signed urls for password protected public links. The share password is used as the signing key and the signed url is then added to the propfind response in the field `downloadURL` which was added before but never used. This change allows owncloud Web to implement a more efficient download mechanism for password protected link shares. --- .../Files/PublicFiles/PublicFilesPlugin.php | 51 +++++++++++++++++++ .../Files/PublicFiles/PublicShareSigner.php | 40 +++++++++++++++ .../Files/PublicFiles/PublicSharingAuth.php | 19 +++++++ .../PublicFiles/PublicShareSignerTest.php | 41 +++++++++++++++ changelog/unreleased/38376 | 5 ++ 5 files changed, 156 insertions(+) create mode 100644 apps/dav/lib/Files/PublicFiles/PublicShareSigner.php create mode 100644 apps/dav/tests/unit/Files/PublicFiles/PublicShareSignerTest.php create mode 100644 changelog/unreleased/38376 diff --git a/apps/dav/lib/Files/PublicFiles/PublicFilesPlugin.php b/apps/dav/lib/Files/PublicFiles/PublicFilesPlugin.php index d0baaf13d06b..5899365d4291 100644 --- a/apps/dav/lib/Files/PublicFiles/PublicFilesPlugin.php +++ b/apps/dav/lib/Files/PublicFiles/PublicFilesPlugin.php @@ -147,6 +147,57 @@ public function propFind(PropFind $propFind, INode $node) { $propFind->handle(FilesPlugin::SIZE_PROPERTYNAME, static function () use ($node) { return $node->getNode()->getSize(); }); + if ($node->getNode()->getType() === FileInfo::TYPE_FILE) { + $server = $this->server; + $propFind->handle(FilesPlugin::DOWNLOADURL_PROPERTYNAME, static function () use ($node, $server) { + $share = $node->getShare(); + $shareNode = $share->getNode(); + // We want to get the relative path of the shared file. + // If the shared resource is a folder e.g. + // - / + // - subfolder/ + // - meme.jpg + // - somefile.txt + // And we want the path of 'meme.jpg' we expect the resource + // path to be '/subfolder/meme.jpg'. + // If the shared resource is a file we can just take the + // name of that file prefixed with a slash like '/cool.gif' + if ($shareNode->getType() === FileInfo::TYPE_FOLDER) { + $shareRoot = $shareNode->getPath(); + $sharedResourcePath = \substr($node->getNode()->getPath(), \strlen($shareRoot)); + } else { + $sharedResourcePath = '/' . $shareNode->getName(); + } + + $path = $server->getBaseUri() . $server->getRequestUri(); + // Let's assume we have this share + // - / + // - subfolder/ + // - meme.jpg + // - somefile.txt + // If the PROPFIND request is done against + // 'remote.php/dav/public-files/{token}/subfolder/meme.jpg' + // then we don't need to append the file name for response as + // it is already in the path. + // Otherwise if the PROPFIND is done against + // 'remote.php/dav/public-files/{token}/subfolder/' + // then we need to append the file name to the path. + if (\substr($path, -\strlen($sharedResourcePath)) !== $sharedResourcePath) { + $path .= '/' . $node->getNode()->getName(); + } + + if ($share->getPassword() === null) { + return $path; + } + + $validUntil = new \DateTime(); + $validUntil->add(new \DateInterval("PT30M")); // valid for 30 minutes + $key = \hash_hkdf('sha256', $share->getPassword()); + + $s = new PublicShareSigner($share->getToken(), $sharedResourcePath, $validUntil, $key); + return $path . '?signature=' . $s->getSignature() . '&expires=' . \urlencode($validUntil->format(\DateTime::ATOM)); + }); + } } } diff --git a/apps/dav/lib/Files/PublicFiles/PublicShareSigner.php b/apps/dav/lib/Files/PublicFiles/PublicShareSigner.php new file mode 100644 index 000000000000..ae33333cbc0e --- /dev/null +++ b/apps/dav/lib/Files/PublicFiles/PublicShareSigner.php @@ -0,0 +1,40 @@ + + * + * @copyright Copyright (c) 2021, 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 + * + */ + +namespace OCA\DAV\Files\PublicFiles; + +class PublicShareSigner { + private $token; + private $fileName; + private $validUntil; + private $signingKey; + + public function __construct(String $token, String $fileName, \DateTime $validUntil, String $signingKey) { + $this->token = $token; + $this->fileName = $fileName; + $this->validUntil = $validUntil->format(\DateTime::ATOM); + $this->signingKey = $signingKey; + } + + public function getSignature() { + return \hash_hmac('sha512/256', \implode('|', [$this->token, $this->fileName, $this->validUntil]), $this->signingKey, false); + } +} diff --git a/apps/dav/lib/Files/PublicFiles/PublicSharingAuth.php b/apps/dav/lib/Files/PublicFiles/PublicSharingAuth.php index 012f7111c4ff..b6eb4848dc94 100644 --- a/apps/dav/lib/Files/PublicFiles/PublicSharingAuth.php +++ b/apps/dav/lib/Files/PublicFiles/PublicSharingAuth.php @@ -101,6 +101,25 @@ public function check(RequestInterface $request, ResponseInterface $response) { return [true, 'principals/system/public']; } + // Clients which don't use cookie based session authentication and want + // to use anchor tags `getQueryParameters(); + if (isset($query['signature'], $query['expires'])) { + $sig = $query['signature']; + $validUntil = \DateTime::createFromFormat(\DateTime::ATOM, $query['expires']); + $now = new \DateTime(); + if ($now < $validUntil) { + $key = \hash_hkdf('sha256', $this->share->getPassword()); + $resource_path = \explode($this->share->getToken(), $request->getPath())[1]; + $s = new PublicShareSigner($this->share->getToken(), $resource_path, $validUntil, $key); + if (\hash_equals($s->getSignature(), $sig)) { + return [true, 'principals/system/public']; + } + } + } + try { return parent::check($request, $response); } catch (LoginException $e) { diff --git a/apps/dav/tests/unit/Files/PublicFiles/PublicShareSignerTest.php b/apps/dav/tests/unit/Files/PublicFiles/PublicShareSignerTest.php new file mode 100644 index 000000000000..d789af5db4c7 --- /dev/null +++ b/apps/dav/tests/unit/Files/PublicFiles/PublicShareSignerTest.php @@ -0,0 +1,41 @@ + + * + * @copyright Copyright (c) 2021, 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 + * + */ + +namespace OCA\DAV\Tests\Unit\Files\PublicFiles; + +use OCA\DAV\Files\PublicFiles\PublicShareSigner; +use Test\TestCase; + +class PublicShareSignerTest extends TestCase { + public function testGet() { + $s = new PublicShareSigner('someToken', 'someFileName', new \DateTime(), 'somekey'); + $hash = $s->getSignature(); + self::assertIsString($hash); + self::assertEquals(64, \strlen($hash)); + } + + public function testVerify() { + $expectedHash = 'd67966402971bd3eb18aea62faf122a30e2dd5c9101aa9e106a56574cc535c6c'; + $date = \DateTime::createFromFormat(\DateTime::ATOM, '2009-01-03T18:15:05Z'); + $s = new PublicShareSigner('someToken', 'someFileName', $date, 'somekey'); + self::assertEquals($expectedHash, $s->getSignature()); + } +} diff --git a/changelog/unreleased/38376 b/changelog/unreleased/38376 new file mode 100644 index 000000000000..0478cddbbcef --- /dev/null +++ b/changelog/unreleased/38376 @@ -0,0 +1,5 @@ +Enhancement: Implement pre-signed download urls for public links + +Added pre-signed download urls for password protected public links to support clients which don't use cookies. + +https://github.com/owncloud/core/pull/38376