Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: SSE files being mistaken for E2EE #593

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 49 additions & 20 deletions lib/E2EEnabledPathCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@
namespace OCA\EndToEndEncryption;

use OCP\Cache\CappedMemoryCache;
use OCP\Files\Cache\ICache;
use OCP\Files\Folder;
use OCP\Files\IHomeStorage;
use OCP\Files\InvalidPathException;
use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\Storage\IStorage;

class E2EEnabledPathCache {
/**
Expand All @@ -45,53 +45,82 @@ public function __construct() {
}

/**
* Checks if the path is an E2E folder or inside an E2E folder
* Checks if the node is an E2EE folder or inside an E2EE folder.
*
* @return bool true, if the node is valid and E2EE, false otherwise
*/
public function isE2EEnabledPath(Node $node): bool {
if ($node->isEncrypted()) {
// only checking for $node->isEncrypted() will lead to false-positives for SSE files,
// as they have the encryption flag set but are not E2E, so we need to check the folder's flag,
// as folders having the encryption flag set are always E2E
if ($node instanceof Folder && $node->isEncrypted()) {
return true;
}
$storage = $node->getStorage();
$cache = $storage->getCache();
return $this->getEncryptedStates($cache, $node, $storage);

try {
$storage = $node->getStorage();
} catch (NotFoundException $e) {
return false;
}
// if not home storage, fallback to EncryptionManager
if (!$storage->instanceOfStorage(IHomeStorage::class)) {
return EncryptionManager::isEncryptedFile($node);
}

// walk path backwards while caching each node's state
return $this->getEncryptedStates((string) $storage->getCache()->getNumericStorageId(), $node);
}

/**
* Get the encryption state for the path
* Determines a node's E2E encryption state by walking up the tree. Caches each node's state on the way.
*
* @param string $storageId the node's storage id, used for caching
* @param Node $node the node to check
*
* @return bool true, if the node is valid and E2EE, false otherwise
*/
protected function getEncryptedStates(ICache $cache, Node $node, IStorage $storage): bool {
if (!$storage->instanceOfStorage(IHomeStorage::class)) {
protected function getEncryptedStates(string $storageId, Node $node): bool {
try {
$nodeId = $node->getId();
} catch (InvalidPathException|NotFoundException $e) {
return false;
}

$storageId = (string)$cache->getNumericStorageId();
// initialize array for storage id if necessary
if (!isset($this->perStorageEncryptedStateCache[$storageId])) {
$this->perStorageEncryptedStateCache[$storageId] = [];
}
if (isset($this->perStorageEncryptedStateCache[$storageId][$node->getId()])) {
return $this->perStorageEncryptedStateCache[$storageId][$node->getId()];
// return cached state if available
elseif (isset($this->perStorageEncryptedStateCache[$storageId][$nodeId])) {
return $this->perStorageEncryptedStateCache[$storageId][$nodeId];
}

if ($node->getPath() === '/') {
// root is never encrypted
$this->perStorageEncryptedStateCache[$storageId][$node->getId()] = false;
$this->perStorageEncryptedStateCache[$storageId][$nodeId] = false;
return false;
}

if ($node->isEncrypted()) {
// no need to go further down in the tree
$this->perStorageEncryptedStateCache[$storageId][$node->getId()] = true;
// checking for folder for the same reason as above
if ($node instanceof Folder && $node->isEncrypted()) {
// no need to go further up in the tree
$this->perStorageEncryptedStateCache[$storageId][$nodeId] = true;
return true;
}

try {
$parentNode = $node->getParent();
} catch (NotFoundException $e) {
$this->perStorageEncryptedStateCache[$storageId][$node->getId()] = false;
// node not encrypted and no parent that could be E2EE, so node is not E2EE
$this->perStorageEncryptedStateCache[$storageId][$nodeId] = false;
return false;
}
$encrypted = $this->getEncryptedStates($cache, $parentNode, $storage);
$this->perStorageEncryptedStateCache[$storageId][$node->getId()] = $encrypted;

// check parent's state
$encrypted = $this->getEncryptedStates($storageId, $parentNode);

// if any parent is E2EE, this node is E2EE as well
$this->perStorageEncryptedStateCache[$storageId][$nodeId] = $encrypted;
return $encrypted;
}
}
8 changes: 7 additions & 1 deletion lib/EncryptionManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,13 @@ public function removeEncryptionFlag(int $id): void {
/**
* Check if a file is in a folder marked as encrypted
*/
public function isEncryptedFile(Node $node): bool {
public static function isEncryptedFile(Node $node): bool {
// traverse up if node is not a folder to prevent false positives for SSE files
// (see E2EEnabledPathCache#isE2EEnabledPath)
if (!($node instanceof Folder)) {
$node = $node->getParent();
}

do {
if ($node->isEncrypted()) {
return true;
Expand Down
27 changes: 6 additions & 21 deletions tests/Unit/Connector/Sabre/LockPluginTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
use OCA\EndToEndEncryption\LockManager;
use OCA\EndToEndEncryption\UserAgentManager;
use OCP\Files\Cache\ICache;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\Storage\IStorage;
use OCP\IUserSession;
Expand Down Expand Up @@ -563,7 +564,7 @@ public function testIsE2EEnabledPathEncryptedFolder():void {
])
->getMock();

$node = $this->createMock(Node::class);
$node = $this->createMock(Folder::class);
$node->expects($this->once())
->method('isEncrypted')
->willReturn(true);
Expand All @@ -586,16 +587,14 @@ public function testIsE2EEnabledPathParentEncrypted():void {
])
->getMock();

$encryptedParentParentNode = $this->createMock(Node::class);
$encryptedParentParentNode = $this->createMock(Folder::class);
$encryptedParentParentNode->expects($this->once())
->method('isEncrypted')
->willReturn(true);
$encryptedParentParentNode->method('getId')
->willReturn(1);
$encryptedParentParentNode->method('getFileInfo')
->willReturn(['parent' => 0]);

$parentNode = $this->createMock(Node::class);
$parentNode = $this->createMock(Folder::class);
$parentNode->expects($this->once())
->method('isEncrypted')
->willReturn(false);
Expand All @@ -604,13 +603,8 @@ public function testIsE2EEnabledPathParentEncrypted():void {
->willReturn($encryptedParentParentNode);
$parentNode->method('getId')
->willReturn(2);
$parentNode->method('getFileInfo')
->willReturn(['parent' => 1]);

$fileNode = $this->createMock(Node::class);
$fileNode->expects($this->atLeastOnce())
->method('isEncrypted')
->willReturn(false);
$cache = $this->createMock(ICache::class);
$cache->method('getNumericStorageId')
->willReturn(1);
Expand Down Expand Up @@ -649,16 +643,14 @@ public function testIsE2EEnabledPathNonEncrypted():void {
])
->getMock();

$encryptedParentParentNode = $this->createMock(Node::class);
$encryptedParentParentNode = $this->createMock(Folder::class);
$encryptedParentParentNode->method('getId')
->willReturn(1);
$encryptedParentParentNode->method('getFileInfo')
->willReturn(['parent' => 0]);
$encryptedParentParentNode->expects($this->once())
->method('getPath')
->willReturn('/');

$parentNode = $this->createMock(Node::class);
$parentNode = $this->createMock(Folder::class);
$parentNode->expects($this->once())
->method('isEncrypted')
->willReturn(false);
Expand All @@ -667,8 +659,6 @@ public function testIsE2EEnabledPathNonEncrypted():void {
->willReturn($encryptedParentParentNode);
$parentNode->method('getId')
->willReturn(2);
$parentNode->method('getFileInfo')
->willReturn(['parent' => 1]);

$cache = $this->createMock(ICache::class);
$cache->method('getNumericStorageId')
Expand All @@ -680,9 +670,6 @@ public function testIsE2EEnabledPathNonEncrypted():void {
->willReturn($cache);

$fileNode = $this->createMock(Node::class);
$fileNode->expects($this->atLeastOnce())
->method('isEncrypted')
->willReturn(false);
$fileNode->expects($this->once())
->method('getParent')
->willReturn($parentNode);
Expand All @@ -691,8 +678,6 @@ public function testIsE2EEnabledPathNonEncrypted():void {
->willReturn('/data/rere/re');
$fileNode->method('getId')
->willReturn(3);
$fileNode->method('getFileInfo')
->willReturn(['parent' => 2]);
$fileNode->method('getStorage')
->willReturn($storage);

Expand Down
4 changes: 2 additions & 2 deletions tests/Unit/EncryptionManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,8 +173,8 @@ public function dataTestIsEncryptedFile(): array {
* @return PHPUnit_Framework_MockObject_MockObject[]
*/
public function constructNestedNodes(): array {
$node1 = $this->getMockBuilder(Node::class)->disableOriginalConstructor()->getMock();
$node2 = $this->getMockBuilder(Node::class)->disableOriginalConstructor()->getMock();
$node1 = $this->getMockBuilder(Folder::class)->disableOriginalConstructor()->getMock();
$node2 = $this->getMockBuilder(Folder::class)->disableOriginalConstructor()->getMock();
$node3 = $this->getMockBuilder(Node::class)->disableOriginalConstructor()->getMock();
$node1->expects($this->any())->method('getParent')->willReturn($node2);
$node1->expects($this->any())->method('getPath')->willReturn('/data/user');
Expand Down
Loading