From 20ca5fd77d0753b58c997ad6a7889153d8b2469e Mon Sep 17 00:00:00 2001 From: Miklos Vajna Date: Mon, 14 Oct 2024 16:11:15 +0200 Subject: [PATCH 1/3] feat: add personal setting to specify the CA chain for document signing Document signing needs to store keys as richdocuments settings. This involves the signing key, certificate and the matching CA chain. As a first step, add code to the personal settings to be able to set a CA chain that issues the signing key / certificate. Setting and getting the setting is possible after this; the setting is not yet exposed in the WOPI CheckFileInfo response. has instructions on how to generate self-signed certificates for document signing for development purposes. Related to #4123 Signed-off-by: Miklos Vajna --- css/admin.scss | 4 ++++ lib/Controller/SettingsController.php | 12 +++++++++++- lib/Service/CapabilitiesService.php | 4 ++++ lib/Settings/Personal.php | 2 ++ src/personal.js | 28 +++++++++++++++++++++++++++ templates/personal.php | 13 +++++++++++++ 6 files changed, 62 insertions(+), 1 deletion(-) diff --git a/css/admin.scss b/css/admin.scss index 0d2a579709..e3288991dd 100644 --- a/css/admin.scss +++ b/css/admin.scss @@ -27,6 +27,10 @@ input#zoteroAPIKeyField { width: 300px; } +textarea#documentSigningCaField { + width: 600px; +} + #richdocuments, #richdocuments-templates { // inline buttons on section headers diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index e2134b47b4..acc5efd0e5 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -236,7 +236,8 @@ public function updateWatermarkSettings($settings = []): JSONResponse { * @return JSONResponse */ public function setPersonalSettings($templateFolder, - $zoteroAPIKeyInput) { + $zoteroAPIKeyInput, + $documentSigningCaInput) { $message = $this->l10n->t('Saved'); $status = 'success'; @@ -257,6 +258,15 @@ public function setPersonalSettings($templateFolder, } } + if ($documentSigningCaInput !== null) { + try { + $this->config->setUserValue($this->userId, 'richdocuments', 'documentSigningCa', $documentSigningCaInput); + } catch (PreConditionNotMetException $e) { + $message = $this->l10n->t('Error when saving'); + $status = 'error'; + } + } + $response = [ 'status' => $status, 'data' => ['message' => $message] diff --git a/lib/Service/CapabilitiesService.php b/lib/Service/CapabilitiesService.php index e15cc7a660..a97c7a234b 100644 --- a/lib/Service/CapabilitiesService.php +++ b/lib/Service/CapabilitiesService.php @@ -85,6 +85,10 @@ public function hasWASMSupport(): bool { return $this->getCapabilities()['hasWASMSupport'] ?? false; } + public function hasDocumentSigningSupport(): bool { + return $this->getCapabilities()['hasDocumentSigningSupport'] ?? false; + } + public function hasFormFilling(): bool { return $this->isVersionAtLeast('24.04.5.2'); } diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php index bd0af6753d..3c8f51c26f 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -34,6 +34,8 @@ public function getForm() { 'personal', [ 'templateFolder' => $this->config->getUserValue($this->userId, 'richdocuments', 'templateFolder', ''), + 'hasDocumentSigningSupport' => $this->capabilitiesService->hasDocumentSigningSupport(), + 'documentSigningCa' => $this->config->getUserValue($this->userId, 'richdocuments', 'documentSigningCa', ''), 'hasZoteroSupport' => $this->capabilitiesService->hasZoteroSupport(), 'zoteroAPIKey' => $this->config->getUserValue($this->userId, 'richdocuments', 'zoteroAPIKey', '') ], diff --git a/src/personal.js b/src/personal.js index bbff0019ae..28e5bcaf16 100644 --- a/src/personal.js +++ b/src/personal.js @@ -17,6 +17,10 @@ import { showError } from '@nextcloud/dialogs' this.zoteroAPIKeySaveButton = document.getElementById('zoteroAPIKeySave') this.zoteroAPIKeyRemoveButton = document.getElementById('zoteroAPIKeyRemove') + this.documentSigningCaInput = document.getElementById('documentSigningCaField') + this.documentSigningCaSaveButton = document.getElementById('documentSigningCaSave') + this.documentSigningCaRemoveButton = document.getElementById('documentSigningCaRemove') + const self = this this.templateSelectButton.addEventListener('click', function() { OC.dialogs.filepicker(t('richdocuments', 'Select a personal template folder'), function(datapath, returntype) { @@ -31,6 +35,12 @@ import { showError } from '@nextcloud/dialogs' }) this.zoteroAPIKeyRemoveButton.addEventListener('click', this.resetZoteroAPI.bind(this)) + + this.documentSigningCaSaveButton.addEventListener('click', function() { + self.updateDocumentSigningCa(self.documentSigningCaInput.value) + }) + + this.documentSigningCaRemoveButton.addEventListener('click', this.resetDocumentSigningCa.bind(this)) } PersonalSettings.prototype.updateSetting = function(path) { @@ -69,6 +79,24 @@ import { showError } from '@nextcloud/dialogs' }) } + PersonalSettings.prototype.updateDocumentSigningCa = function(ca) { + const self = this + this._updateSetting({ documentSigningCaInput: ca }, function() { + self.documentSigningCaInput.value = ca + }, function() { + showError(t('richdocuments', 'Failed to update the document signing CA chain')) + }) + } + + PersonalSettings.prototype.resetDocumentSigningCa = function() { + const self = this + this._updateSetting({ documentSigningCaInput: '' }, function() { + self.documentSigningCaInput.value = '' + }, function() { + + }) + } + PersonalSettings.prototype._updateSetting = function(data, successCallback, errorCallback) { OC.msg.startAction('#documents-admin-msg', t('richdocuments', 'Saving …')) const request = new XMLHttpRequest() diff --git a/templates/personal.php b/templates/personal.php index b5e08ee81e..ac35a384e3 100644 --- a/templates/personal.php +++ b/templates/personal.php @@ -30,5 +30,18 @@

t('This instance does not support Zotero, because the feature is missing or disabled. Please contact the administration.')); ?>

+

t('Document signing')) ?>

+ +
+


+
+ + +

+

t('To use document signing, specify your signing certificate, key and CA chain here.')); ?>

+
+ +

t('This instance does not support document signing, because the feature is missing or disabled. Please contact the administrator.')); ?>

+ From 6ca8071c495ebf4cd7797dd44a0700f9970abc31 Mon Sep 17 00:00:00 2001 From: Miklos Vajna Date: Tue, 15 Oct 2024 09:31:39 +0200 Subject: [PATCH 2/3] feat: expose the documentSigningCa personal setting in the WOPI CheckFileInfo This setting was already possible to read and write from the personal settings UI, but was not available towards Collabora Online. Other private user settings like the Zotero API key are exposed in the WOPI CheckFileInfo reply. Do the same here: if the feature is enabled in general and this is not a public share, then include the signature CA setting in the CheckFileInfo response. The same still needs doing for the signature cert/key. Related to #4123 Signed-off-by: Miklos Vajna --- lib/Controller/WopiController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php index 1c900cca58..fdbe14ff09 100644 --- a/lib/Controller/WopiController.php +++ b/lib/Controller/WopiController.php @@ -165,6 +165,11 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons $zoteroAPIKey = $this->config->getUserValue($wopi->getEditorUid(), 'richdocuments', 'zoteroAPIKey', ''); $response['UserPrivateInfo']['ZoteroAPIKey'] = $zoteroAPIKey; } + $enableDocumentSigning = $this->config->getAppValue(Application::APPNAME, 'documentSigningEnabled', 'yes') === 'yes'; + if (!$isPublic && $enableDocumentSigning) { + $documentSigningCa = $this->config->getUserValue($wopi->getEditorUid(), 'richdocuments', 'documentSigningCa', ''); + $response['UserPrivateInfo']['SignatureCa'] = $documentSigningCa; + } if ($wopi->hasTemplateId()) { $response['TemplateSource'] = $this->getWopiUrlForTemplate($wopi); } From adfb9056d40af3a861463753f47e72ab5f2f905c Mon Sep 17 00:00:00 2001 From: Miklos Vajna Date: Wed, 16 Oct 2024 09:15:21 +0200 Subject: [PATCH 3/3] feat: document signing, add setting for the signing certificate & key, too (fixes #4123) The CA chain for the document signing was already a user setting & it was exposed in the WOPI CheckFileInfo, but the actual signing certificate & key was missing, so signing was not possible. These are typically in a similar PEM format using just ASCII characters, so providing a textarea where the user can paste them sounds like a good fit. Add the read/write of this setting and also expose it as part of the private user info in WOPI CheckFileInfo. With this, once all 3 are configured, it's possible to sign a document in Nextcloud Office, using the Signature button on the Home tab of the notebookbar. Signed-off-by: Miklos Vajna --- css/admin.scss | 8 +++++ lib/Controller/SettingsController.php | 18 ++++++++++ lib/Controller/WopiController.php | 4 +++ lib/Settings/Personal.php | 2 ++ src/personal.js | 51 ++++++++++++++++++++++++++- templates/personal.php | 10 ++++++ 6 files changed, 92 insertions(+), 1 deletion(-) diff --git a/css/admin.scss b/css/admin.scss index e3288991dd..19822c4112 100644 --- a/css/admin.scss +++ b/css/admin.scss @@ -27,6 +27,14 @@ input#zoteroAPIKeyField { width: 300px; } +textarea#documentSigningCertField { + width: 600px; +} + +textarea#documentSigningKeyField { + width: 600px; +} + textarea#documentSigningCaField { width: 600px; } diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index acc5efd0e5..0ae90da07a 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -237,6 +237,8 @@ public function updateWatermarkSettings($settings = []): JSONResponse { */ public function setPersonalSettings($templateFolder, $zoteroAPIKeyInput, + $documentSigningCertInput, + $documentSigningKeyInput, $documentSigningCaInput) { $message = $this->l10n->t('Saved'); $status = 'success'; @@ -258,6 +260,22 @@ public function setPersonalSettings($templateFolder, } } + if ($documentSigningCertInput !== null) { + try { + $this->config->setUserValue($this->userId, 'richdocuments', 'documentSigningCert', $documentSigningCertInput); + } catch (PreConditionNotMetException $e) { + $message = $this->l10n->t('Error when saving'); + $status = 'error'; + } + } + if ($documentSigningKeyInput !== null) { + try { + $this->config->setUserValue($this->userId, 'richdocuments', 'documentSigningKey', $documentSigningKeyInput); + } catch (PreConditionNotMetException $e) { + $message = $this->l10n->t('Error when saving'); + $status = 'error'; + } + } if ($documentSigningCaInput !== null) { try { $this->config->setUserValue($this->userId, 'richdocuments', 'documentSigningCa', $documentSigningCaInput); diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php index fdbe14ff09..1abedf8708 100644 --- a/lib/Controller/WopiController.php +++ b/lib/Controller/WopiController.php @@ -167,6 +167,10 @@ public function checkFileInfo(string $fileId, string $access_token): JSONRespons } $enableDocumentSigning = $this->config->getAppValue(Application::APPNAME, 'documentSigningEnabled', 'yes') === 'yes'; if (!$isPublic && $enableDocumentSigning) { + $documentSigningCert = $this->config->getUserValue($wopi->getEditorUid(), 'richdocuments', 'documentSigningCert', ''); + $response['UserPrivateInfo']['SignatureCert'] = $documentSigningCert; + $documentSigningKey = $this->config->getUserValue($wopi->getEditorUid(), 'richdocuments', 'documentSigningKey', ''); + $response['UserPrivateInfo']['SignatureKey'] = $documentSigningKey; $documentSigningCa = $this->config->getUserValue($wopi->getEditorUid(), 'richdocuments', 'documentSigningCa', ''); $response['UserPrivateInfo']['SignatureCa'] = $documentSigningCa; } diff --git a/lib/Settings/Personal.php b/lib/Settings/Personal.php index 3c8f51c26f..a6e45228d2 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -35,6 +35,8 @@ public function getForm() { [ 'templateFolder' => $this->config->getUserValue($this->userId, 'richdocuments', 'templateFolder', ''), 'hasDocumentSigningSupport' => $this->capabilitiesService->hasDocumentSigningSupport(), + 'documentSigningCert' => $this->config->getUserValue($this->userId, 'richdocuments', 'documentSigningCert', ''), + 'documentSigningKey' => $this->config->getUserValue($this->userId, 'richdocuments', 'documentSigningKey', ''), 'documentSigningCa' => $this->config->getUserValue($this->userId, 'richdocuments', 'documentSigningCa', ''), 'hasZoteroSupport' => $this->capabilitiesService->hasZoteroSupport(), 'zoteroAPIKey' => $this->config->getUserValue($this->userId, 'richdocuments', 'zoteroAPIKey', '') diff --git a/src/personal.js b/src/personal.js index 28e5bcaf16..e2e89a5c09 100644 --- a/src/personal.js +++ b/src/personal.js @@ -17,6 +17,12 @@ import { showError } from '@nextcloud/dialogs' this.zoteroAPIKeySaveButton = document.getElementById('zoteroAPIKeySave') this.zoteroAPIKeyRemoveButton = document.getElementById('zoteroAPIKeyRemove') + this.documentSigningCertInput = document.getElementById('documentSigningCertField') + this.documentSigningCertSaveButton = document.getElementById('documentSigningCertSave') + this.documentSigningCertRemoveButton = document.getElementById('documentSigningCertRemove') + this.documentSigningKeyInput = document.getElementById('documentSigningKeyField') + this.documentSigningKeySaveButton = document.getElementById('documentSigningKeySave') + this.documentSigningKeyRemoveButton = document.getElementById('documentSigningKeyRemove') this.documentSigningCaInput = document.getElementById('documentSigningCaField') this.documentSigningCaSaveButton = document.getElementById('documentSigningCaSave') this.documentSigningCaRemoveButton = document.getElementById('documentSigningCaRemove') @@ -36,10 +42,17 @@ import { showError } from '@nextcloud/dialogs' this.zoteroAPIKeyRemoveButton.addEventListener('click', this.resetZoteroAPI.bind(this)) + this.documentSigningCertSaveButton.addEventListener('click', function() { + self.updateDocumentSigningCert(self.documentSigningCertInput.value) + }) + this.documentSigningCertRemoveButton.addEventListener('click', this.resetDocumentSigningCert.bind(this)) + this.documentSigningKeySaveButton.addEventListener('click', function() { + self.updateDocumentSigningKey(self.documentSigningKeyInput.value) + }) + this.documentSigningKeyRemoveButton.addEventListener('click', this.resetDocumentSigningKey.bind(this)) this.documentSigningCaSaveButton.addEventListener('click', function() { self.updateDocumentSigningCa(self.documentSigningCaInput.value) }) - this.documentSigningCaRemoveButton.addEventListener('click', this.resetDocumentSigningCa.bind(this)) } @@ -79,6 +92,42 @@ import { showError } from '@nextcloud/dialogs' }) } + PersonalSettings.prototype.updateDocumentSigningCert = function(ca) { + const self = this + this._updateSetting({ documentSigningCertInput: ca }, function() { + self.documentSigningCertInput.value = ca + }, function() { + showError(t('richdocuments', 'Failed to update the document signing CA chain')) + }) + } + + PersonalSettings.prototype.resetDocumentSigningCert = function() { + const self = this + this._updateSetting({ documentSigningCertInput: '' }, function() { + self.documentSigningCertInput.value = '' + }, function() { + + }) + } + + PersonalSettings.prototype.updateDocumentSigningKey = function(ca) { + const self = this + this._updateSetting({ documentSigningKeyInput: ca }, function() { + self.documentSigningKeyInput.value = ca + }, function() { + showError(t('richdocuments', 'Failed to update the document signing CA chain')) + }) + } + + PersonalSettings.prototype.resetDocumentSigningKey = function() { + const self = this + this._updateSetting({ documentSigningKeyInput: '' }, function() { + self.documentSigningKeyInput.value = '' + }, function() { + + }) + } + PersonalSettings.prototype.updateDocumentSigningCa = function(ca) { const self = this this._updateSetting({ documentSigningCaInput: ca }, function() { diff --git a/templates/personal.php b/templates/personal.php index ac35a384e3..9cf81a4180 100644 --- a/templates/personal.php +++ b/templates/personal.php @@ -33,6 +33,16 @@

t('Document signing')) ?>

+


+
+ + +

+


+
+ + +