From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 8CE941FF136 for ; Mon, 20 Apr 2026 18:17:06 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 589F996CF; Mon, 20 Apr 2026 18:16:52 +0200 (CEST) From: Christian Ebner To: pbs-devel@lists.proxmox.com Subject: [PATCH proxmox-backup v4 21/30] ui: expose assigning encryption key to sync jobs Date: Mon, 20 Apr 2026 18:15:24 +0200 Message-ID: <20260420161533.1055484-22-c.ebner@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20260420161533.1055484-1-c.ebner@proxmox.com> References: <20260420161533.1055484-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: 1776701667701 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.071 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: J4GXCLNDMRX6M5PYLPMJH6CW5PYYE224 X-Message-ID-Hash: J4GXCLNDMRX6M5PYLPMJH6CW5PYYE224 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 --- 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..510c3f89c 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( + 'When pushing, the system uses the active encryption key to encrypt unencrypted sources snapshots. It leaves existing encrypted content as-is, and skips partially encrypted content.', + ), + cbind: { + hidden: '{!syncDirectionPush}', + }, + }, + { + xtype: 'box', + style: { + 'inline-size': '325px', + 'overflow-wrap': 'break-word', + }, + padding: '5', + html: gettext( + 'To prevent premature removal, associated keys hold a reference to a key until you explicitly unlink it. When you change your active encryption key, the system automatically associates the old key to protect it from accidental deletion, ensuring you can still decrypt older contents.', + ), + cbind: { + hidden: '{!syncDirectionPush}', + }, + }, + ], + }, ], }, }); -- 2.47.3