From 18a8d7a5e05d2c6973d253db9810087795fe9212 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 (cherry picked from commit 20ca5fd77d0753b58c997ad6a7889153d8b2469e) --- 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 d2eebe3a57..e4af5ca6cd 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -235,7 +235,8 @@ public function updateWatermarkSettings($settings = []): JSONResponse { * @return JSONResponse */ public function setPersonalSettings($templateFolder, - $zoteroAPIKeyInput) { + $zoteroAPIKeyInput, + $documentSigningCaInput) { $message = $this->l10n->t('Saved'); $status = 'success'; @@ -256,6 +257,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 03c544597c..352b89ab73 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -45,6 +45,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 af6bcc875e266f37de8c4cb28060f187a94de93e 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 (cherry picked from commit 6ca8071c495ebf4cd7797dd44a0700f9970abc31) --- lib/Controller/WopiController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/Controller/WopiController.php b/lib/Controller/WopiController.php index a075a2bc83..a64f52cf6b 100644 --- a/lib/Controller/WopiController.php +++ b/lib/Controller/WopiController.php @@ -173,6 +173,11 @@ public function checkFileInfo($fileId, $access_token) { $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 c3f1bb3e8399addf025f550dbbccb97844db661b 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 (cherry picked from commit adfb9056d40af3a861463753f47e72ab5f2f905c) --- 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 e4af5ca6cd..cb72d91b85 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -236,6 +236,8 @@ public function updateWatermarkSettings($settings = []): JSONResponse { */ public function setPersonalSettings($templateFolder, $zoteroAPIKeyInput, + $documentSigningCertInput, + $documentSigningKeyInput, $documentSigningCaInput) { $message = $this->l10n->t('Saved'); $status = 'success'; @@ -257,6 +259,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 a64f52cf6b..d93f16c91d 100644 --- a/lib/Controller/WopiController.php +++ b/lib/Controller/WopiController.php @@ -175,6 +175,10 @@ public function checkFileInfo($fileId, $access_token) { } $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 352b89ab73..7a70aaa3cf 100644 --- a/lib/Settings/Personal.php +++ b/lib/Settings/Personal.php @@ -46,6 +46,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')) ?>

+


+
+ + +

+


+
+ + +