Skip to content

Commit fe5a646

Browse files
committed
feat: Add config switch to enable E2EE in browser
Signed-off-by: Louis Chemineau <[email protected]>
1 parent 7a18ba1 commit fe5a646

File tree

6 files changed

+191
-48
lines changed

6 files changed

+191
-48
lines changed

lib/Controller/ConfigController.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
6+
* SPDX-License-Identifier: AGPL-3.0-or-later
7+
*/
8+
namespace OCA\EndToEndEncryption\Controller;
9+
10+
use OCA\EndToEndEncryption\AppInfo\Application;
11+
use OCP\AppFramework\Controller;
12+
use OCP\AppFramework\Http;
13+
use OCP\AppFramework\Http\Attribute\FrontpageRoute;
14+
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
15+
use OCP\AppFramework\Http\JSONResponse;
16+
use OCP\IConfig;
17+
use OCP\IRequest;
18+
19+
class ConfigController extends Controller {
20+
21+
public function __construct(
22+
string $appName,
23+
IRequest $request,
24+
private IConfig $config,
25+
private ?string $userId,
26+
) {
27+
parent::__construct($appName, $request);
28+
}
29+
30+
#[NoAdminRequired]
31+
#[FrontpageRoute(verb: 'PUT', url: '/api/v1/config/{key}')]
32+
public function setUserConfig(string $key, string $value): JSONResponse {
33+
if (is_null($this->userId)) {
34+
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
35+
}
36+
37+
if (!in_array($key, ['e2eeInBrowserEnabled'])) {
38+
return new JSONResponse([], Http::STATUS_PRECONDITION_FAILED);
39+
}
40+
41+
$this->config->setUserValue($this->userId, Application::APP_ID, $key, $value);
42+
return new JSONResponse([], Http::STATUS_OK);
43+
}
44+
}

lib/Listener/LoadAdditionalListener.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,36 @@
1010

1111
use OCA\EndToEndEncryption\AppInfo\Application;
1212
use OCA\Files\Event\LoadAdditionalScriptsEvent;
13+
use OCP\AppFramework\Services\IInitialState;
1314
use OCP\EventDispatcher\Event;
1415
use OCP\EventDispatcher\IEventListener;
16+
use OCP\IConfig;
1517
use OCP\Util;
1618

1719
/**
1820
* @template-implements IEventListener<LoadAdditionalScriptsEvent>
1921
*/
2022
class LoadAdditionalListener implements IEventListener {
2123

22-
public function __construct() {
24+
public function __construct(
25+
private IInitialState $initialState,
26+
private IConfig $config,
27+
private ?string $userId,
28+
) {
2329
}
2430

2531
public function handle(Event $event): void {
2632
if (!($event instanceof LoadAdditionalScriptsEvent)) {
2733
return;
2834
}
2935

36+
$this->initialState->provideInitialState(
37+
'userConfig',
38+
[
39+
'e2eeInBrowserEnabled' => $this->config->getUserValue($this->userId, 'end_to_end_encryption', 'e2eeInBrowserEnabled', 'false') === 'true',
40+
]
41+
);
42+
3043
Util::addInitScript(Application::APP_ID, 'end_to_end_encryption-files');
3144
}
3245
}

lib/Settings/Personal.php

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,19 @@
1414
use OCA\EndToEndEncryption\IKeyStorage;
1515
use OCP\AppFramework\Http\TemplateResponse;
1616
use OCP\AppFramework\Services\IInitialState;
17+
use OCP\IConfig;
1718
use OCP\IUserSession;
1819
use OCP\Settings\ISettings;
1920

2021
class Personal implements ISettings {
21-
private IKeyStorage $keyStorage;
22-
private IInitialState $initialState;
23-
private ?string $userId;
24-
private IUserSession $userSession;
25-
private Config $config;
26-
27-
public function __construct(IKeyStorage $keyStorage, IInitialState $initialState, ?string $userId, IUserSession $userSession, Config $config) {
28-
$this->keyStorage = $keyStorage;
29-
$this->initialState = $initialState;
30-
$this->userId = $userId;
31-
$this->config = $config;
32-
$this->userSession = $userSession;
22+
public function __construct(
23+
private IKeyStorage $keyStorage,
24+
private IInitialState $initialState,
25+
private ?string $userId,
26+
private IUserSession $userSession,
27+
private Config $e2eConfig,
28+
private IConfig $config,
29+
) {
3330
}
3431

3532
public function getForm(): TemplateResponse {
@@ -39,10 +36,17 @@ public function getForm(): TemplateResponse {
3936
&& $this->keyStorage->privateKeyExists($this->userId);
4037
$this->initialState->provideInitialState('hasKey', $hasKey);
4138

39+
$this->initialState->provideInitialState(
40+
'userConfig',
41+
[
42+
'e2eeInBrowserEnabled' => $this->config->getUserValue($this->userId, 'end_to_end_encryption', 'e2eeInBrowserEnabled', 'false') === 'true',
43+
]
44+
);
45+
4246
return new TemplateResponse(
4347
Application::APP_ID,
4448
'settings',
45-
['canUseApp' => !$this->config->isDisabledForUser($this->userSession->getUser())]
49+
['canUseApp' => !$this->e2eConfig->isDisabledForUser($this->userSession->getUser())]
4650
);
4751
}
4852

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
<!--
2-
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3-
- SPDX-License-Identifier: AGPL-3.0-or-later
4-
-->
2+
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<!-- eslint-disable jsdoc/require-jsdoc -->
7+
58
<script setup lang="ts">
69
import { computed, ref } from 'vue'
710
811
import { t } from '@nextcloud/l10n'
912
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js'
1013
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
14+
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
15+
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
1116
1217
const emit = defineEmits<{
1318
(e: 'close', mnemonic: string): void
1419
}>()
1520
21+
const dialogRef = ref()
1622
const mnemonic = ref('')
23+
const confirmToggle = ref(false)
1724
18-
/**
19-
*
20-
*/
21-
function submit() {
22-
// TODO: Implement form validation
23-
if (mnemonic.value.trim().split(/\s+/g).length !== 12) {
24-
throw new Error('Mnemonic must be 12 words long')
25-
}
25+
const isFormValid = computed(() => confirmToggle.value === true && mnemonic.value.trim().split(/\s+/g).length === 12)
2626
27+
function submit() {
2728
emit('close', mnemonic.value)
2829
}
2930
@@ -32,18 +33,34 @@ const buttons = computed(() => [
3233
label: t('end_to_en_encryption', 'Submit'),
3334
nativeType: 'submit',
3435
type: 'primary',
36+
disabled: !isFormValid.value,
3537
callback: submit,
3638
},
3739
])
3840
</script>
3941
<template>
40-
<NcDialog :name="t('end_to_end_encryption', 'Enter your 12 words mnemonic')"
42+
<NcDialog ref="dialogRef"
43+
:name="t('end_to_end_encryption', 'Enter your 12 words mnemonic')"
4144
:buttons="buttons"
4245
:is-form="true"
4346
@submit="submit">
47+
<NcNoteCard type="warning"
48+
:show-alert="true"
49+
:heading="t('end_to_end_encryption', 'Decrypting your files in the browser can weaken security')">
50+
{{ t('end_to_end_encryption', 'The server could serve malicious source code to extract the secret that protects your files.') }}
51+
52+
<NcCheckboxRadioSwitch v-model="confirmToggle"
53+
required="true"
54+
data-cy-e2ee-mnemonic-prompt="i_understand_the_risks"
55+
type="switch">
56+
{{ t('end_to_end_encryption', 'I understand the risks') }}
57+
</NcCheckboxRadioSwitch>
58+
</NcNoteCard>
59+
4460
<NcTextField :value.sync="mnemonic"
61+
required="true"
62+
pattern="^(\w+\s+){11}\w+$"
4563
:label="t('end_to_end_encryption', 'Mnemonic')"
46-
:autofocus="true"
47-
:required="true" />
64+
:autofocus="true" />
4865
</NcDialog>
4966
</template>

src/components/SecuritySection.vue

Lines changed: 78 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,60 @@
66
<template>
77
<NcSettingsSection :name="t('end_to_end_encryption', 'End-to-end encryption')"
88
:description="encryptionState">
9-
<NcButton :disabled="!hasKey || shouldDisplayWarning"
9+
<NcButton v-if="!shouldDisplayE2EEInBrowserWarning && userConfig['e2eeInBrowserEnabled'] === false"
10+
class="margin-bottom"
11+
:disabled="!hasKey"
12+
type="secondary"
13+
@click="shouldDisplayE2EEInBrowserWarning = true">
14+
{{ t('end_to_end_encryption', 'Enable E2EE navigation in browser') }}
15+
</NcButton>
16+
<NcNoteCard v-else
17+
class="notecard"
18+
type="warning"
19+
:show-alert="true"
20+
:heading="t('end_to_end_encryption', 'Enabling E2EE in the browser can weaken security')">
21+
<NcButton v-if="userConfig['e2eeInBrowserEnabled'] === false"
22+
class="close-button"
23+
:aria-label="t('end_to_end_encryption', 'Close')"
24+
type="tertiary-no-background"
25+
@click="shouldDisplayE2EEInBrowserWarning = false">
26+
<template #icon>
27+
<IconClose :size="20" />
28+
</template>
29+
</NcButton>
30+
31+
{{ t('end_to_end_encryption', 'The server could serve malicious source code to extract the secret that protects your files.') }}
32+
33+
<NcCheckboxRadioSwitch :disabled="!hasKey"
34+
data-cy-e2ee-settings-setting="e2ee_in_browser_enabled"
35+
:checked="userConfig.e2eeInBrowserEnabled"
36+
class="margin-bottom"
37+
type="switch"
38+
@update:checked="value => setConfig('e2eeInBrowserEnabled', value)">
39+
{{ t('end_to_end_encryption', 'Enable E2EE navigation in browser') }}
40+
</NcCheckboxRadioSwitch>
41+
</NcNoteCard>
42+
43+
<NcButton v-if="!shouldDisplayWarning"
44+
:disabled="!hasKey"
1045
:type="(hasKey && !shouldDisplayWarning) ? 'error' : 'secondary'"
1146
@click="startResetProcess()">
1247
{{ t('end_to_end_encryption', 'Reset end-to-end encryption') }}
1348
</NcButton>
49+
<NcNoteCard v-else
50+
class="notecard"
51+
type="warning"
52+
:show-alert="true"
53+
:heading="t('end_to_end_encryption', 'Please read carefully before resetting your end-to-end encryption keys')">
54+
<NcButton class="close-button"
55+
:aria-label="t('end_to_end_encryption', 'Close')"
56+
type="tertiary-no-background"
57+
@click="shouldDisplayWarning = false">
58+
<template #icon>
59+
<IconClose :size="20" />
60+
</template>
61+
</NcButton>
1462

15-
<div v-if="shouldDisplayWarning && hasKey" class="notecard warning" role="alert">
16-
<p><strong>{{ t('end_to_end_encryption', 'Please read carefully before resetting your end-to-end encryption keys') }}</strong></p>
1763
<ul>
1864
<li>{{ t('end_to_end_encryption', 'Once your end-to-end encryption keys are reset, all files stored in your encrypted folder will be inaccessible.') }}</li>
1965
<li>{{ t('end_to_end_encryption', 'You should only reset your end-to-end encryption keys if you lost your secure key words (mnemonic).') }}</li>
@@ -28,34 +74,43 @@
2874
<NcButton type="error" @click="showDialog">
2975
{{ t('end_to_end_encryption', "Confirm and reset end-to-end encryption") }}
3076
</NcButton>
31-
</div>
77+
</NcNoteCard>
3278
</NcSettingsSection>
3379
</template>
3480

35-
<script>
81+
<script lang="ts">
82+
import { defineComponent } from 'vue'
83+
import IconClose from 'vue-material-design-icons/Close.vue'
84+
3685
import axios from '@nextcloud/axios'
86+
import { translate as t } from '@nextcloud/l10n'
3787
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
3888
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
3989
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
90+
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
4091
import { loadState } from '@nextcloud/initial-state'
4192
import { showError, showSuccess, DialogBuilder } from '@nextcloud/dialogs'
42-
import { generateOcsUrl } from '@nextcloud/router'
93+
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
4394
4495
import logger from '../services/logger.js'
4596
46-
export default {
97+
export default defineComponent({
4798
name: 'SecuritySection',
4899
components: {
49100
NcSettingsSection,
50101
NcButton,
51102
NcCheckboxRadioSwitch,
103+
NcNoteCard,
104+
IconClose,
52105
},
53106
54107
data() {
55108
return {
56109
hasKey: loadState('end_to_end_encryption', 'hasKey'),
57110
shouldDisplayWarning: false,
58111
deleteEncryptedFiles: false,
112+
shouldDisplayE2EEInBrowserWarning: false,
113+
userConfig: loadState('end_to_end_encryption', 'userConfig', { e2eeInBrowserEnabled: false }),
59114
}
60115
},
61116
@@ -178,23 +233,27 @@ export default {
178233
}
179234
return true
180235
},
236+
237+
async setConfig(key: string, value: string) {
238+
await axios.put(generateUrl('apps/end_to_end_encryption/api/v1/config/{key}', { key }), {
239+
value: (typeof value === 'string') ? value : JSON.stringify(value),
240+
})
241+
this.userConfig[key] = value
242+
},
243+
244+
t,
181245
},
182-
}
246+
})
183247
</script>
184248

185249
<style lang="scss" scoped>
186250
.notecard {
187-
color: var(--color-text-light) !important;
188-
background-color: var(--note-background) !important;
189-
border: 1px solid var(--color-border);
190-
border-left: 4px solid var(--note-theme);
191-
border-radius: var(--border-radius);
192-
box-shadow: rgba(43, 42, 51, 0.05) 0 1px 2px 0;
193-
margin: 1rem 0;
194-
padding: 1rem !important;
195-
&.warning {
196-
--note-background: rgba(var(--color-warning-rgb), 0.2);
197-
--note-theme: var(--color-warning);
251+
position: relative;
252+
253+
.close-button {
254+
position: absolute;
255+
top: 0;
256+
right: 0;
198257
}
199258
}
200259

src/files.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6+
import { loadState } from '@nextcloud/initial-state'
7+
68
import { setupWebDavDecryptionProxy } from './services/webDavProxy.ts'
79

8-
setupWebDavDecryptionProxy()
10+
const userConfig = loadState('end_to_end_encryption', 'userConfig', { e2eeInBrowserEnabled: false })
11+
12+
if (userConfig.e2eeInBrowserEnabled) {
13+
setupWebDavDecryptionProxy()
14+
}

0 commit comments

Comments
 (0)