Skip to content

Commit

Permalink
Cache end to end encrypted paths
Browse files Browse the repository at this point in the history
Signed-off-by: Carl Schwan <carl@carlschwan.eu>
  • Loading branch information
CarlSchwan committed Aug 16, 2022
1 parent ace8a27 commit b8b9c93
Show file tree
Hide file tree
Showing 7 changed files with 145 additions and 38 deletions.
48 changes: 20 additions & 28 deletions lib/Connector/Sabre/APlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\DAV\Connector\Sabre\File;
use OCA\EndToEndEncryption\E2EEnabledPathCache;
use OCP\Files\FileInfo;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
Expand All @@ -41,6 +42,7 @@ abstract class APlugin extends ServerPlugin {
protected ?Server $server = null;
protected IRootFolder $rootFolder;
protected IUserSession $userSession;
protected E2EEnabledPathCache $pathCache;

/**
* Should plugin be applied to the current node?
Expand All @@ -50,14 +52,15 @@ abstract class APlugin extends ServerPlugin {

/**
* APlugin constructor.
*
* @param IRootFolder $rootFolder
* @param IUserSession $userSession
*/
public function __construct(IRootFolder $rootFolder,
IUserSession $userSession) {
public function __construct(
IRootFolder $rootFolder,
IUserSession $userSession,
E2EEnabledPathCache $pathCache
) {
$this->rootFolder = $rootFolder;
$this->userSession = $userSession;
$this->pathCache = $pathCache;
}

/**
Expand Down Expand Up @@ -117,6 +120,18 @@ protected function getFileNode(string $path): Node {
}
}

/**
* Checks if the path is an E2E folder or inside an E2E folder
*/
protected function isE2EEnabledPath(string $path): bool {
try {
$node = $this->getFileNode($path);
} catch (NotFound $e) {
return false;
}
return $this->pathCache->isE2EEnabledPath($node, $path);
}

/**
* Check if we process a file or directory. This plugin should ignore calendars
* and contacts
Expand All @@ -132,27 +147,4 @@ protected function isFile(string $url, INode $node): bool {
return $this->applyPlugin[$url];
}

/**
* Checks if the path is an E2E folder or inside an E2E folder
*/
protected function isE2EEnabledPath(string $path):bool {
try {
$node = $this->getFileNode($path);
} catch (NotFound $e) {
return false;
}

while ($node->isEncrypted() === false || $node->getType() === FileInfo::TYPE_FILE) {
$node = $node->getParent();

// Nitpick: This doesn't check if root is E2E,
// but that's not supported at the moment anyway
if ($node->getPath() === '/') {
// top-level folder reached
return false;
}
}

return true;
}
}
6 changes: 4 additions & 2 deletions lib/Connector/Sabre/LockPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
use Sabre\DAV\INode;
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use OCA\EndToEndEncryption\E2EEnabledPathCache;

class LockPlugin extends APlugin {
private LockManager $lockManager;
Expand All @@ -47,8 +48,9 @@ class LockPlugin extends APlugin {
public function __construct(IRootFolder $rootFolder,
IUserSession $userSession,
LockManager $lockManager,
UserAgentManager $userAgentManager) {
parent::__construct($rootFolder, $userSession);
UserAgentManager $userAgentManager,
E2EEnabledPathCache $pathCache) {
parent::__construct($rootFolder, $userSession, $pathCache);
$this->lockManager = $lockManager;
$this->userAgentManager = $userAgentManager;
}
Expand Down
6 changes: 4 additions & 2 deletions lib/Connector/Sabre/PropFindPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use OCA\DAV\Connector\Sabre\Directory;
use OCA\DAV\Connector\Sabre\Exception\Forbidden;
use OCA\EndToEndEncryption\UserAgentManager;
use OCA\EndToEndEncryption\E2EEnabledPathCache;
use OCP\Files\IRootFolder;
use OCP\IRequest;
use OCP\IUserSession;
Expand All @@ -43,8 +44,9 @@ class PropFindPlugin extends APlugin {
public function __construct(IRootFolder $rootFolder,
IUserSession $userSession,
UserAgentManager $userAgentManager,
IRequest $request) {
parent::__construct($rootFolder, $userSession);
IRequest $request,
E2EEnabledPathCache $pathCache) {
parent::__construct($rootFolder, $userSession, $pathCache);
$this->userAgentManager = $userAgentManager;
$this->request = $request;
}
Expand Down
1 change: 1 addition & 0 deletions lib/Connector/Sabre/RedirectRequestPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use Sabre\DAV\Server;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;
use OCA\EndToEndEncryption\E2EEnabledPathCache;

/**
* Class WritePlugin
Expand Down
102 changes: 102 additions & 0 deletions lib/E2EEnabledPathCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

// SPDX-FileCopyrightText: 2020 Georg Ehrke <georg-nextcloud@ehrke.email>
// SPDX-License-Identifier: AGPL-3.0-or-later

namespace OCA\EndToEndEncryption;

use Sabre\DAV\INode;
use OCP\Files\Node;
use OCP\Files\IHomeStorage;
use OCP\Files\Cache\ICache;
use OC\Files\Storage\Wrapper\Wrapper;
use OCA\Files_Sharing\SharedStorage;

class E2EEnabledPathCache {
/**
* @psalm-type FileId=int
*
* @psalm-type EncryptedState=array{0: FileId, 1: bool}
*
* @psalm-type Path=string
*
* @psalm-type StorageId=string|int
*/

/** @var array<StorageId, array<Path, EncryptedState>> */
protected $cacheEntries;

/**
* Checks if the path is an E2E folder or inside an E2E folder
*
* @param INode&Node $node
*/
public function isE2EEnabledPath($node, string $path): bool {
$storage = $node->getStorage();
$cache = $storage->getCache();
$encryptedStates = $this->getEncryptedStates($cache, $path, $storage, !$storage->instanceOfStorage(IHomeStorage::class) || $storage->instanceOfStorage(SharedStorage::class));
foreach ($encryptedStates as [$fileid, $encryptedState]) {
if ($encryptedState) {
return true;
}
}
return false;
}

/**
* Get the file ids of the given path and its parents
*
* @return array<Path, EncryptedState>
*/
protected function getEncryptedStates(ICache $cache, string $path, $storage, bool $isExternalStorage): array {
/** @psalm-suppress InvalidArgument */
if ($storage->instanceOfStorage(\OCA\GroupFolders\Mount\GroupFolderStorage::class)) {
// Special implementation for groupfolder since all groupfolders share the same storage
// id so add the group folder id in the cache key too.
$groupFolderStorage = $storage;
if ($this->storage instanceof Wrapper) {
$groupFolderStorage = $getInstanceOfStorage(\OCA\GroupFolders\Mount\GroupFolderStorage::class);
}
if ($groupFolderStorage === null) {
throw new \LogicException('Should not happen: Storage is instance of GroupFolderStorage but no group folder storage found while unwrapping.');
}
/**
* @psalm-suppress UndefinedDocblockClass
* @psalm-suppress UndefinedInterfaceMethod
*/
$cacheId = $cache->getNumericStorageId() . '/' . $groupFolderStorage->getFolderId();
} else {
$cacheId = $cache->getNumericStorageId();
}
if (isset($this->cacheEntries[$cacheId][$path])) {
return $this->cacheEntries[$cacheId][$path];
}

$parentIds = [];
if ($path !== $this->dirname($path)) {
$cacheEntries = [];
$cacheEntry = $cache->get($path);
if ($cacheEntry !== false) {
$cacheEntries[] = [$cacheEntry->getId(), $cacheEntry->isEncrypted()];
if ($cacheEntry->isEncrypted()) {
// no need to go further down in the tree
$this->cacheEntries[$cacheId][$path] = $parentEntries;
return $cacheEntry;
}
}
$cacheEntries = array_merge($this->getEncryptedStates($cache, $this->dirname($path), $storage, $isExternalStorage), $cacheEntries);
} elseif (!$isExternalStorage) {
return [];
}

$this->cacheEntries[$cacheId][$path] = $cacheEntries;
return $cacheEntries;
}

protected function dirname(string $path): string {
$dir = dirname($path);
return $dir === '.' ? '' : $dir;
}
}
10 changes: 7 additions & 3 deletions tests/Unit/Connector/Sabre/LockPluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use OCA\EndToEndEncryption\Connector\Sabre\LockPlugin;
use OCA\EndToEndEncryption\LockManager;
use OCA\EndToEndEncryption\UserAgentManager;
use OCA\EndToEndEncryption\E2EEnabledPathCache;
use OCP\Files\FileInfo;
use OCP\Files\IRootFolder;
use OCP\Files\Node;
Expand All @@ -56,8 +57,10 @@ class LockPluginTest extends TestCase {
/** @var UserAgentManager|\PHPUnit\Framework\MockObject\MockObject */
private $userAgentManager;

/** @var LockPlugin */
private $plugin;
/** @var E2EEnabledPathCache|\PHPUnit\Framework\MockObject\MockObject */
private $pathCache;

private LockPlugin $plugin;

protected function setUp(): void {
parent::setUp();
Expand All @@ -66,9 +69,10 @@ protected function setUp(): void {
$this->userSession = $this->createMock(IUserSession::class);
$this->lockManager = $this->createMock(LockManager::class);
$this->userAgentManager = $this->createMock(UserAgentManager::class);
$this->pathCache = $this->createMock(E2EEnabledPathCache::class);

$this->plugin = new LockPlugin($this->rootFolder, $this->userSession,
$this->lockManager, $this->userAgentManager);
$this->lockManager, $this->userAgentManager, $this->pathCache);
}

public function testInitialize(): void {
Expand Down
10 changes: 7 additions & 3 deletions tests/Unit/Connector/Sabre/RedirectRequestPluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

use OCA\DAV\Connector\Sabre\File;
use OCA\EndToEndEncryption\Connector\Sabre\RedirectRequestPlugin;
use OCA\EndToEndEncryption\E2EEnabledPathCache;
use OCP\Files\IRootFolder;
use OCP\IUserSession;
use Sabre\DAV\Server;
Expand All @@ -39,16 +40,19 @@ class RedirectRequestPluginTest extends TestCase {
/** @var IUserSession|\PHPUnit\Framework\MockObject\MockObject */
private $userSession;

/** @var RedirectRequestPlugin */
private $plugin;
/** @var E2EEnabledPathCache|\PHPUnit\Framework\MockObject\MockObject */
private $pathCache;

private RedirectRequestPlugin $plugin;

protected function setUp(): void {
parent::setUp();

$this->rootFolder = $this->createMock(IRootFolder::class);
$this->userSession = $this->createMock(IUserSession::class);
$this->pathCache = $this->createMock(E2EEnabledPathCache::class);

$this->plugin = new RedirectRequestPlugin($this->rootFolder, $this->userSession);
$this->plugin = new RedirectRequestPlugin($this->rootFolder, $this->userSession, $this->pathCache);
}

public function testInitialize(): void {
Expand Down

0 comments on commit b8b9c93

Please sign in to comment.