From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 58E8E1FF137 for ; Tue, 14 Apr 2026 14:59:49 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id B82D618EA6; Tue, 14 Apr 2026 15:00:20 +0200 (CEST) From: Christian Ebner To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox-backup v3 21/30] ui: expose assigning encryption key to sync jobs Date: Tue, 14 Apr 2026 14:59:14 +0200 Message-ID: <20260414125923.892345-22-c.ebner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260414125923.892345-1-c.ebner@proxmox.com> References: <20260414125923.892345-1-c.ebner@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1776171501686 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.070 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: HD6HIPBRZKC6MWC6F7ZPFJG4DYOAL5MY X-Message-ID-Hash: HD6HIPBRZKC6MWC6F7ZPFJG4DYOAL5MY X-MailFrom: c.ebner@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox Backup Server development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: This allows to select pre-defined encryption keys and assign them to the sync job configuration. Sync keys can be either assigned as active encryption key to sync jobs in push direction, to be used when pushing new contents or associated to a sync job in pull direction, then used to decrypt contents with matching key fingerprint. As active encryption key only ones with are not archived can be used, while associations can be made also with archived keys, to still be able do decrypt contents on pull and to avoid key deletion if associated to either push or pull sync jobs. Signed-off-by: Christian Ebner --- changes since version 2: - switch field label for associated keys based on sync direction. - add comment field explaining active encryption key and associated keys and their relation on key rotation. www/Makefile | 1 + www/form/EncryptionKeySelector.js | 96 +++++++++++++++++++++++++++++++ www/window/SyncJobEdit.js | 62 ++++++++++++++++++++ 3 files changed, 159 insertions(+) create mode 100644 www/form/EncryptionKeySelector.js diff --git a/www/Makefile b/www/Makefile index 08ad50846..51da9d74e 100644 --- a/www/Makefile +++ b/www/Makefile @@ -55,6 +55,7 @@ JSSRC= \ form/GroupSelector.js \ form/GroupFilter.js \ form/VerifyOutdatedAfter.js \ + form/EncryptionKeySelector.js \ data/RunningTasksStore.js \ button/TaskButton.js \ panel/PrunePanel.js \ diff --git a/www/form/EncryptionKeySelector.js b/www/form/EncryptionKeySelector.js new file mode 100644 index 000000000..e0390e56a --- /dev/null +++ b/www/form/EncryptionKeySelector.js @@ -0,0 +1,96 @@ +Ext.define('PBS.form.EncryptionKeySelector', { + extend: 'Ext.form.field.ComboBox', + alias: 'widget.pbsEncryptionKeySelector', + + queryMode: 'local', + + valueField: 'id', + displayField: 'id', + + emptyText: gettext('None'), + + listConfig: { + columns: [ + { + dataIndex: 'id', + header: gettext('Key ID'), + sortable: true, + flex: 1, + }, + { + dataIndex: 'created', + header: gettext('Created'), + sortable: true, + renderer: Proxmox.Utils.render_timestamp, + flex: 1, + }, + { + dataIndex: 'archived-at', + header: gettext('Archived'), + renderer: (val) => (val ? Proxmox.Utils.render_timestamp(val) : ''), + sortable: true, + flex: 1, + }, + ], + emptyText: `
${gettext('No key accessible.')}
`, + }, + + config: { + deleteEmpty: true, + extraRequestParams: {}, + }, + // override framework function to implement deleteEmpty behaviour + getSubmitData: function () { + let me = this; + + let data = null; + if (!me.disabled && me.submitValue) { + let val = me.getSubmitValue(); + if (val !== null && val !== '') { + data = {}; + data[me.getName()] = val; + } else if (me.getDeleteEmpty()) { + data = {}; + data.delete = me.getName(); + } + } + + return data; + }, + + triggers: { + clear: { + cls: 'pmx-clear-trigger', + weight: -1, + hidden: true, + handler: function () { + this.triggers.clear.setVisible(false); + this.setValue(''); + }, + }, + }, + + listeners: { + change: function (field, value) { + let canClear = (value ?? '') !== ''; + field.triggers.clear.setVisible(canClear); + }, + }, + + initComponent: function () { + let me = this; + + me.store = Ext.create('Ext.data.Store', { + model: 'pbs-encryption-keys', + autoLoad: true, + proxy: { + type: 'proxmox', + timeout: 30 * 1000, + url: `/api2/json/config/encryption-keys`, + extraParams: me.extraRequestParams, + }, + }); + + me.callParent(); + }, +}); diff --git a/www/window/SyncJobEdit.js b/www/window/SyncJobEdit.js index 074c7855a..a944a6395 100644 --- a/www/window/SyncJobEdit.js +++ b/www/window/SyncJobEdit.js @@ -34,10 +34,12 @@ Ext.define('PBS.window.SyncJobEdit', { if (me.syncDirection === 'push') { me.subject = gettext('Sync Job - Push Direction'); me.syncDirectionPush = true; + me.syncCryptKeyMultiSelect = false; me.syncRemoteLabel = gettext('Target Remote'); me.syncRemoteDatastore = gettext('Target Datastore'); me.syncRemoteNamespace = gettext('Target Namespace'); me.syncLocalOwner = gettext('Local User'); + me.associatedKeysLabel = gettext('Associated Keys'); // Sync direction request parameter is only required for creating new jobs, // for edit and delete it is derived from the job config given by it's id. if (me.isCreate) { @@ -52,6 +54,7 @@ Ext.define('PBS.window.SyncJobEdit', { me.syncRemoteDatastore = gettext('Source Datastore'); me.syncRemoteNamespace = gettext('Source Namespace'); me.syncLocalOwner = gettext('Local Owner'); + me.associatedKeysLabel = gettext('Decryption Keys'); } return {}; @@ -560,6 +563,65 @@ Ext.define('PBS.window.SyncJobEdit', { }, ], }, + { + xtype: 'inputpanel', + title: gettext('Encryption'), + column1: [ + { + xtype: 'pbsEncryptionKeySelector', + name: 'active-encryption-key', + fieldLabel: gettext('Active Encryption Key'), + multiSelect: false, + cbind: { + deleteEmpty: '{!isCreate}', + disabled: '{!syncDirectionPush}', + hidden: '{!syncDirectionPush}', + }, + }, + { + xtype: 'pbsEncryptionKeySelector', + name: 'associated-key', + multiSelect: true, + cbind: { + fieldLabel: '{associatedKeysLabel}', + deleteEmpty: '{!isCreate}', + }, + extraRequestParams: { + 'include-archived': true, + }, + }, + ], + column2: [ + { + xtype: 'box', + style: { + 'inline-size': '325px', + 'overflow-wrap': 'break-word', + }, + padding: '5', + html: gettext( + 'Active encryption key is used to encrypt snapshots which are not encrypted on the source during sync. Already encrypted contents are unaffected, partially encrypted contents skipped if set.', + ), + cbind: { + hidden: '{!syncDirectionPush}', + }, + }, + { + xtype: 'box', + style: { + 'inline-size': '325px', + 'overflow-wrap': 'break-word', + }, + padding: '5', + html: gettext( + 'Associated keys store a reference to keys in order to protect them from removal without prior disassociation. On changing the active encryption key, the previous key is added to the associated keys in order to protect from accidental deletion in case it still is required to decrypt contents.', + ), + cbind: { + hidden: '{!syncDirectionPush}', + }, + }, + ], + }, ], }, }); -- 2.47.3