Skip to content

Commit 60e10e2

Browse files
authored
Merge pull request #867 from nextcloud/artonge/feat/allow_loading_media_as_blob
feat: Allow loading media as `blob:`
2 parents 8b363c5 + b5d0cab commit 60e10e2

8 files changed

Lines changed: 48 additions & 8 deletions

js/end_to_end_encryption-files.mjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

js/end_to_end_encryption-files.mjs.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/AppInfo/Application.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use OCA\EndToEndEncryption\IMetaDataStorage;
1919
use OCA\EndToEndEncryption\IMetaDataStorageV1;
2020
use OCA\EndToEndEncryption\KeyStorage;
21+
use OCA\EndToEndEncryption\Listener\AllowBlobMediaInCSPListener;
2122
use OCA\EndToEndEncryption\Listener\LoadAdditionalListener;
2223
use OCA\EndToEndEncryption\Listener\UserDeletedListener;
2324
use OCA\EndToEndEncryption\MetaDataStorage;
@@ -34,6 +35,7 @@
3435
use OCP\AppFramework\Bootstrap\IRegistrationContext;
3536
use OCP\EventDispatcher\IEventDispatcher;
3637
use OCP\SabrePluginEvent;
38+
use OCP\Security\CSP\AddContentSecurityPolicyEvent;
3739
use OCP\User\Events\UserDeletedEvent;
3840

3941
class Application extends App implements IBootstrap {
@@ -62,6 +64,7 @@ public function register(IRegistrationContext $context): void {
6264
$context->registerServiceAlias(IMetaDataStorage::class, MetaDataStorage::class);
6365
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
6466
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);
67+
$context->registerEventListener(AddContentSecurityPolicyEvent::class, AllowBlobMediaInCSPListener::class);
6568
$context->registerPublicShareTemplateProvider(E2EEPublicShareTemplateProvider::class);
6669
}
6770

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OCA\EndToEndEncryption\Listener;
11+
12+
use OCP\AppFramework\Http\ContentSecurityPolicy;
13+
use OCP\EventDispatcher\Event;
14+
use OCP\EventDispatcher\IEventListener;
15+
use OCP\Security\CSP\AddContentSecurityPolicyEvent;
16+
17+
/**
18+
* @template-implements IEventListener<Event>
19+
* We need it to be able to expose decrypted video and audio files in a blob URL.
20+
*/
21+
class AllowBlobMediaInCSPListener implements IEventListener {
22+
23+
public function handle(Event $event): void {
24+
if (!($event instanceof AddContentSecurityPolicyEvent)) {
25+
return;
26+
}
27+
28+
$csp = new ContentSecurityPolicy();
29+
$csp->addAllowedMediaDomain('blob:');
30+
$event->addPolicy($csp);
31+
}
32+
}

src/files.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ if (userConfig.e2eeInBrowserEnabled) {
2626
}
2727

2828
function disableFileAction(actionId: string) {
29-
logger.debug('Disabling file action', { actionId })
29+
logger.debug(`Inhibiting ${actionId} actions for e2ee files`)
3030
const actions = getFileActions()
3131

32-
const action = actions.find(action => action.id === actionId) as any
32+
const action = actions.find(action => action.id === actionId) as unknown as { _action: { enabled: (nodes: Node[], view: View) => boolean } }
3333
const originalEnabled = action._action.enabled
3434

3535
action._action.enabled = (nodes: Node[], view: View) => {

src/services/downloadUnencryptedAction.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

@@ -13,6 +13,7 @@ import { FileAction, Node, FileType, DefaultType } from '@nextcloud/files'
1313
import { isDownloadable } from './permissions.ts'
1414

1515
async function downloadNodes([file]: Node[]) {
16+
// Decryption happens in the proxy.
1617
const response = await fetch(file.encodedSource)
1718
const decryptedFileContent = await response.arrayBuffer()
1819
const blob = new Blob([decryptedFileContent], { type: file.mime })

src/services/state.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import { dirname } from 'path'
8+
import type { WebDAVClient } from 'webdav'
89

910
import { getCurrentUser } from '@nextcloud/auth'
1011
import { getClient, getDefaultPropfind } from '@nextcloud/files/dav'
@@ -17,7 +18,7 @@ import { decryptMetadataInfo, getMetadataPrivateKey } from './metadataUtils.ts'
1718
import logger from './logger.ts'
1819
import { validateMetadataSignature, validateUserCertificates } from './security.ts'
1920

20-
const davClient = getClient()
21+
const davClient = getClient() as WebDAVClient
2122

2223
export const state = {
2324
_userPrivateKey: undefined as CryptoKey | undefined,

src/services/webDavProxy.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ async function handleGet(request: Request): Promise<Response> {
5858
throw new Error('Could not find file in metadata')
5959
}
6060

61-
logger.debug('Fetching encrypted file', { request })
6261
return await decryptFile(await responsePromise, fileInfo)
6362
} catch (error) {
6463
return await responsePromise
@@ -144,11 +143,15 @@ export function replacePlaceholdersInPropfind(xml: DAVResult, path: string, decr
144143
}
145144

146145
export async function decryptFile(response: Response, fileEncryptionInfo: FileEncryptionInfo): Promise<Response> {
146+
logger.debug('Decrypting encrypted file', { response, fileEncryptionInfo })
147147
const decryptedFileContent = await decryptWithAES(
148148
new Uint8Array(await response.arrayBuffer()),
149149
await loadAESPrivateKey(base64ToBuffer(fileEncryptionInfo.key)),
150150
{ iv: base64ToBuffer(fileEncryptionInfo.nonce) },
151151
)
152152

153-
return new Response(decryptedFileContent, response)
153+
const headers = new Headers(response.headers)
154+
headers.set('Content-Type', fileEncryptionInfo.mimetype)
155+
156+
return new Response(decryptedFileContent, { ...response, headers })
154157
}

0 commit comments

Comments
 (0)