Skip to content
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
5 changes: 5 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
use OCA\EndToEndEncryption\KeyStorage;
use OCA\EndToEndEncryption\Listener\AllowBlobMediaInCSPListener;
use OCA\EndToEndEncryption\Listener\LoadAdditionalListener;
use OCA\EndToEndEncryption\Listener\ShareDeletedListener;
use OCA\EndToEndEncryption\Listener\UserDeletedListener;
use OCA\EndToEndEncryption\MetaDataStorage;
use OCA\EndToEndEncryption\MetaDataStorageV1;
use OCA\EndToEndEncryption\Middleware\CanUseAppMiddleware;
use OCA\EndToEndEncryption\Middleware\ClientHasCapabilityMiddleware;
use OCA\EndToEndEncryption\Middleware\UserAgentCheckMiddleware;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
use OCA\Files_Sharing\Event\BeforeTemplateRenderedEvent;
use OCA\Files_Trashbin\Events\MoveToTrashEvent;
use OCA\Files_Versions\Events\CreateVersionEvent;
use OCP\AppFramework\App;
Expand All @@ -36,6 +38,7 @@
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Security\CSP\AddContentSecurityPolicyEvent;
use OCP\Share\Events\ShareDeletedEvent;
use OCP\User\Events\UserDeletedEvent;
use Override;

Expand All @@ -61,6 +64,8 @@ public function register(IRegistrationContext $context): void {
$context->registerServiceAlias(IMetaDataStorageV1::class, MetaDataStorageV1::class);
$context->registerServiceAlias(IMetaDataStorage::class, MetaDataStorage::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerEventListener(ShareDeletedEvent::class, ShareDeletedListener::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, LoadAdditionalListener::class);
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);
$context->registerEventListener(AddContentSecurityPolicyEvent::class, AllowBlobMediaInCSPListener::class);
$context->registerPublicShareTemplateProvider(E2EEPublicShareTemplateProvider::class);
Expand Down
2 changes: 1 addition & 1 deletion lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public function getCapabilities(): array {
return [
'end-to-end-encryption' => [
'enabled' => true,
'api-version' => '2.0',
'api-version' => '2.1',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still unsure about that API bump

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to communicate this to the clients so they aware the server supports this new API.
Quickly checked clients source code and it seems they only check the major version at the moment so this should be safe (as its backwards compatible).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's see then, worth case, it forces us to be better at version management

'keys-exist' => $keysExist,
]
];
Expand Down
12 changes: 4 additions & 8 deletions lib/Connector/Sabre/PropFindPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
use OCA\DAV\Connector\Sabre\File;
use OCA\EndToEndEncryption\IMetaDataStorage;
use OCA\EndToEndEncryption\UserAgentManager;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IRequest;
use OCP\IUserSession;
Expand All @@ -37,7 +36,6 @@ public function __construct(
private UserAgentManager $userAgentManager,
private IRequest $request,
private IMetaDataStorage $metaDataStorage,
private ?Folder $userFolder,
) {
parent::__construct($rootFolder, $userSession);
}
Expand Down Expand Up @@ -68,7 +66,7 @@ public function setE2EEProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
$propFind->handle(self::E2EE_METADATA_PROPERTYNAME, function () use ($node) {
if ($this->isE2EEnabledPath($node)) {
return $this->metaDataStorage->getMetaData(
$this->userSession->getUser()->getUID(),
($this->userSession->getUser() ?? $node->getNode()->getOwner())->getUID(),
$node->getId(),
);
}
Expand All @@ -82,11 +80,9 @@ public function setE2EEProperties(PropFind $propFind, \Sabre\DAV\INode $node) {
}

// This property was introduced to expose encryption status for both files and folders.
if ($this->userFolder !== null) {
$propFind->handle(self::E2EE_IS_ENCRYPTED, function () use ($node) {
return $this->isE2EEnabledPath($node) ? '1' : '0';
});
}
$propFind->handle(self::E2EE_IS_ENCRYPTED, function () use ($node) {
return $this->isE2EEnabledPath($node) ? '1' : '0';
});
}

/**
Expand Down
98 changes: 57 additions & 41 deletions lib/Controller/KeyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@

use BadMethodCallException;
use Exception;
use OCA\EndToEndEncryption\AppInfo\Application;
use OCA\EndToEndEncryption\Exceptions\KeyExistsException;
use OCA\EndToEndEncryption\IKeyStorage;
use OCA\EndToEndEncryption\SignatureHandler;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSBadRequestException;
use OCP\AppFramework\OCS\OCSForbiddenException;
Expand All @@ -23,47 +26,45 @@
use OCP\Files\NotPermittedException;
use OCP\IL10N;
use OCP\IRequest;
use OCP\Share\IManager;
use Psr\Log\LoggerInterface;

class KeyController extends OCSController {
private ?string $userId;
private IKeyStorage $keyStorage;
private SignatureHandler $signatureHandler;
private LoggerInterface $logger;
private IL10N $l10n;

public function __construct(string $AppName,
public function __construct(
IRequest $request,
?string $userId,
IKeyStorage $keyStorage,
SignatureHandler $signatureHandler,
LoggerInterface $logger,
IL10N $l10n,
private ?string $userId,
private IKeyStorage $keyStorage,
private SignatureHandler $signatureHandler,
private LoggerInterface $logger,
private IL10N $l10n,
private IManager $shareManager,
) {
parent::__construct($AppName, $request);
$this->userId = $userId;
$this->keyStorage = $keyStorage;
$this->signatureHandler = $signatureHandler;
$this->logger = $logger;
$this->l10n = $l10n;
parent::__construct(Application::APP_ID, $request);
}

/**
* Get private key
*
* @NoAdminRequired
* @E2ERestrictUserAgent
*
* @param ?string $shareToken - Optional share token to get a private key associated with a share
* @return DataResponse<Http::STATUS_OK, array{private-key: string}, array{}>
* @throws OCSBadRequestException Internal error
* @throws OCSForbiddenException Not allowed to get private key
* @throws OCSNotFoundException Private key not found
*
* 200: Private key returned
*/
public function getPrivateKey(): DataResponse {
#[NoAdminRequired]
#[PublicPage]
public function getPrivateKey(?string $shareToken = null): DataResponse {
if ($this->userId === null && $shareToken === null) {
throw new OCSForbiddenException($this->l10n->t('Not allowed to get private key'));
}

try {
$privateKey = $this->keyStorage->getPrivateKey($this->userId);
$privateKey = $this->keyStorage->getPrivateKey($this->userId ?? '', $shareToken);
return new DataResponse(['private-key' => $privateKey]);
} catch (ForbiddenException $e) {
throw new OCSForbiddenException($this->l10n->t('This is someone else\'s private key'));
Expand All @@ -81,16 +82,17 @@ public function getPrivateKey(): DataResponse {
*
* @NoAdminRequired
*
* @param ?string $shareToken - Optional share token to delete a private key associated with a share
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
* @throws OCSBadRequestException Internal error
* @throws OCSForbiddenException Not allowed to delete public key
* @throws OCSNotFoundException Private key not found
*
* 200: Private key deleted successfully
*/
public function deletePrivateKey(): DataResponse {
public function deletePrivateKey(?string $shareToken = null): DataResponse {
try {
$this->keyStorage->deletePrivateKey($this->userId);
$this->keyStorage->deletePrivateKey($this->userId, $shareToken);
return new DataResponse();
} catch (NotPermittedException $e) {
throw new OCSForbiddenException($this->l10n->t('You are not allowed to delete this private key'));
Expand All @@ -109,16 +111,17 @@ public function deletePrivateKey(): DataResponse {
* @NoAdminRequired
* @E2ERestrictUserAgent
*
* @param string $privateKey The new private key
* @param string $privateKey - The new private key
* @param ?string $shareToken - Optional share token to set a private key associated with a share
* @return DataResponse<Http::STATUS_OK, array{private-key: string}, array{}>|DataResponse<Http::STATUS_CONFLICT, list<empty>, array{}>
* @throws OCSBadRequestException Internal error
*
* 200: Private key set successfully
* 409: Private key already exists
*/
public function setPrivateKey(string $privateKey): DataResponse {
public function setPrivateKey(string $privateKey, ?string $shareToken = null): DataResponse {
try {
$this->keyStorage->setPrivateKey($privateKey, $this->userId);
$this->keyStorage->setPrivateKey($privateKey, $this->userId, $shareToken);
} catch (KeyExistsException $e) {
return new DataResponse([], Http::STATUS_CONFLICT);
} catch (Exception $e) {
Expand All @@ -132,7 +135,6 @@ public function setPrivateKey(string $privateKey): DataResponse {
/**
* Get public key
*
* @NoAdminRequired
* @E2ERestrictUserAgent
* @param string $users a json encoded list of users
* @return DataResponse<Http::STATUS_OK, array{public-keys: array<string, string>}, array{}>
Expand All @@ -141,9 +143,10 @@ public function setPrivateKey(string $privateKey): DataResponse {
*
* 200: Public keys returned
*/
#[NoAdminRequired]
#[PublicPage]
public function getPublicKeys(string $users = ''): DataResponse {
$usersArray = $this->jsonDecode($users);

$result = ['public-keys' => []];
foreach ($usersArray as $uid) {
try {
Expand All @@ -170,6 +173,7 @@ public function getPublicKeys(string $users = ''): DataResponse {
* @E2ERestrictUserAgent
*
* @param string $csr request to create a valid public key
* @param ?string $shareToken - optional share token to create a public key associated with a share
*
* @return DataResponse<Http::STATUS_OK, array{public-key: string}, array{}>|DataResponse<Http::STATUS_CONFLICT, list<empty>, array{}>
* @throws OCSForbiddenException Common name (CN) does not match the current user
Expand All @@ -178,13 +182,29 @@ public function getPublicKeys(string $users = ''): DataResponse {
* 200: Public key created successfully
* 409: Public key already exists
*/
public function createPublicKey(string $csr): DataResponse {
if ($this->keyStorage->publicKeyExists($this->userId)) {
public function createPublicKey(string $csr, ?string $shareToken = null): DataResponse {
if ($this->keyStorage->publicKeyExists($this->userId, $shareToken)) {
return new DataResponse([], Http::STATUS_CONFLICT);
}

$subject = openssl_csr_get_subject($csr);
if ($subject === false) {
throw new OCSBadRequestException($this->l10n->t('Could not parse the CSR, please make sure to submit a valid CSR'));
}
$cn = isset($subject['CN']) ? $subject['CN'] : '';
if ($shareToken !== null) {
if ($cn !== "s:$shareToken") {
throw new OCSForbiddenException($this->l10n->t('Common name (CN) does not match the share token'));
}
$share = $this->shareManager->getShareByToken($shareToken);
if ($share->getShareOwner() !== $this->userId) {
throw new OCSForbiddenException($this->l10n->t('You are not the owner of the share'));
}
} elseif ($this->userId !== $cn) {
throw new OCSForbiddenException($this->l10n->t('Common name (CN) does not match the current user'));
}

try {
$subject = openssl_csr_get_subject($csr);
$publicKey = $this->signatureHandler->sign($csr);
} catch (BadMethodCallException $e) {
$this->logger->critical($e->getMessage(), ['exception' => $e, 'app' => $this->appName]);
Expand All @@ -194,13 +214,7 @@ public function createPublicKey(string $csr): DataResponse {
throw new OCSBadRequestException($this->l10n->t('Internal error'));
}

$cn = isset($subject['CN']) ? $subject['CN'] : '';
if ($cn !== $this->userId) {
throw new OCSForbiddenException($this->l10n->t('Common name (CN) does not match the current user'));
}

$this->keyStorage->setPublicKey($publicKey, $this->userId);

$this->keyStorage->setPublicKey($publicKey, $this->userId, $shareToken);
return new DataResponse(['public-key' => $publicKey]);
}

Expand Down Expand Up @@ -235,6 +249,7 @@ public function setPublicKey(string $publicKey): DataResponse {
*
* @NoAdminRequired
*
* @param ?string $shareToken - Optional share token to delete a public key associated with a share
* @return DataResponse<Http::STATUS_OK, list<empty>, array{}>
*
* @throws OCSForbiddenException Not allowed to delete public key
Expand All @@ -243,9 +258,9 @@ public function setPublicKey(string $publicKey): DataResponse {
*
* 200: Public key deleted successfully
*/
public function deletePublicKey(): ?DataResponse {
public function deletePublicKey(?string $shareToken = null): ?DataResponse {
try {
$this->keyStorage->deletePublicKey($this->userId);
$this->keyStorage->deletePublicKey($this->userId, $shareToken);
return new DataResponse();
} catch (NotFoundException $e) {
throw new OCSNotFoundException($this->l10n->t('Could not find the public key belonging to %s', [$this->userId]));
Expand All @@ -259,7 +274,6 @@ public function deletePublicKey(): ?DataResponse {


/**
* @NoAdminRequired
* @E2ERestrictUserAgent
*
* Get the public server key so that the clients can verify the
Expand All @@ -271,6 +285,8 @@ public function deletePublicKey(): ?DataResponse {
*
* 200: Server public key returned
*/
#[NoAdminRequired]
#[PublicPage]
public function getPublicServerKey(): DataResponse {
try {
$publicKey = $this->signatureHandler->getPublicServerKey();
Expand Down Expand Up @@ -300,7 +316,7 @@ private function jsonDecode(string $users): array {
}
}

if (!in_array($this->userId, $usersArray, true)) {
if ($this->userId !== null && !in_array($this->userId, $usersArray, true)) {
$usersArray[] = $this->userId;
}

Expand Down
6 changes: 5 additions & 1 deletion lib/E2EEPublicShareTemplateProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use OCP\AppFramework\Http\Template\PublicTemplateResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\Constants;
use OCP\Defaults;
use OCP\Files\FileInfo;
use OCP\Files\NotFoundException;
Expand All @@ -38,7 +39,10 @@ public function __construct(

public function shouldRespond(IShare $share): bool {
$node = $share->getNode();
return $node->getType() === FileInfo::TYPE_FOLDER && $node->isEncrypted();

return $node->getType() === FileInfo::TYPE_FOLDER
&& $node->isEncrypted()
&& ($share->getPermissions() & Constants::PERMISSION_READ) === 0;
}

protected function getMetadata(IShare $share): array {
Expand Down
Loading
Loading