all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Stoiko Ivanov <s.ivanov@proxmox.com>
To: pmg-devel@lists.proxmox.com
Subject: [PATCH pmg-gui 12/15] fix #3326: ui: pbs remote: add encryption tab to edit window
Date: Wed,  3 Jun 2026 20:03:14 +0200	[thread overview]
Message-ID: <20260603180445.98770-13-s.ivanov@proxmox.com> (raw)
In-Reply-To: <20260603180445.98770-1-s.ivanov@proxmox.com>

code is taken from pve-manager/www/manager6/storage/PBSEdit.js
and minimally adapted (onlineHelp property, renaming of utils class).

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 js/PBSRemoteEdit.js | 255 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 255 insertions(+)

diff --git a/js/PBSRemoteEdit.js b/js/PBSRemoteEdit.js
index 8ccc39c..aa51184 100644
--- a/js/PBSRemoteEdit.js
+++ b/js/PBSRemoteEdit.js
@@ -1,3 +1,253 @@
+Ext.define('PMG.panel.PBSEncryptionKeyTab', {
+    extend: 'Proxmox.panel.InputPanel',
+    xtype: 'pmgPBSEncryptionKeyTab',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    onlineHelp: 'pmgbackup_pbs_remotes',
+
+    onGetValues: function (form) {
+        let values = {};
+        if (form.cryptMode === 'upload') {
+            values['encryption-key'] = form['crypt-key-upload'];
+        } else if (form.cryptMode === 'autogenerate') {
+            values['encryption-key'] = 'autogen';
+        } else if (form.cryptMode === 'none') {
+            if (!this.isCreate) {
+                values.delete = ['encryption-key'];
+            }
+        }
+        return values;
+    },
+
+    setValues: function (values) {
+        let me = this;
+        let vm = me.getViewModel();
+
+        let cryptKeyInfo = values['encryption-key'];
+        if (cryptKeyInfo) {
+            let icon = '<span class="fa fa-lock good"></span> ';
+            if (cryptKeyInfo.match(/^[a-fA-F0-9]{2}:/)) {
+                // new style fingerprint
+                let shortKeyFP = PMG.Utils.render_pbs_fingerprint(cryptKeyInfo);
+                values['crypt-key-fp'] =
+                    icon + `${gettext('Active')} - ${gettext('Fingerprint')} ${shortKeyFP}`;
+            } else {
+                // old key without FP
+                values['crypt-key-fp'] = icon + gettext('Active');
+            }
+            values.cryptMode = 'keep';
+            values['crypt-allow-edit'] = false;
+        } else {
+            values['crypt-key-fp'] = gettext('None');
+            let cryptModeNone = me.down('radiofield[inputValue=none]');
+            cryptModeNone.setBoxLabel(gettext('Do not encrypt backups'));
+            values.cryptMode = 'none';
+            values['crypt-allow-edit'] = true;
+        }
+        vm.set('keepCryptVisible', !!cryptKeyInfo);
+        vm.set('allowEdit', !cryptKeyInfo);
+
+        me.callParent([values]);
+    },
+
+    viewModel: {
+        data: {
+            allowEdit: true,
+            keepCryptVisible: false,
+        },
+        formulas: {
+            showDangerousHint: (get) => {
+                let allowEdit = get('allowEdit');
+                return get('keepCryptVisible') && allowEdit;
+            },
+        },
+    },
+
+    items: [
+        {
+            xtype: 'displayfield',
+            name: 'crypt-key-fp',
+            fieldLabel: gettext('Encryption Key'),
+            padding: '2 0',
+        },
+        {
+            xtype: 'checkbox',
+            name: 'crypt-allow-edit',
+            boxLabel: gettext('Edit existing encryption key (dangerous!)'),
+            hidden: true,
+            submitValue: false,
+            isDirty: () => false,
+            bind: {
+                hidden: '{!keepCryptVisible}',
+                value: '{allowEdit}',
+            },
+        },
+        {
+            xtype: 'radiofield',
+            name: 'cryptMode',
+            inputValue: 'keep',
+            boxLabel: gettext('Keep encryption key'),
+            padding: '0 0 0 25',
+            cbind: {
+                hidden: '{isCreate}',
+            },
+            bind: {
+                hidden: '{!keepCryptVisible}',
+                disabled: '{!allowEdit}',
+            },
+        },
+        {
+            xtype: 'radiofield',
+            name: 'cryptMode',
+            inputValue: 'none',
+            checked: true,
+            padding: '0 0 0 25',
+            cbind: {
+                disabled: '{!isCreate}',
+                checked: '{isCreate}',
+                boxLabel: (get) =>
+                    get('isCreate')
+                        ? gettext('Do not encrypt backups')
+                        : gettext('Delete existing encryption key'),
+            },
+            bind: {
+                disabled: '{!allowEdit}',
+            },
+        },
+        {
+            xtype: 'radiofield',
+            name: 'cryptMode',
+            inputValue: 'autogenerate',
+            boxLabel: gettext('Auto-generate a client encryption key'),
+            padding: '0 0 0 25',
+            cbind: {
+                disabled: '{!isCreate}',
+            },
+            bind: {
+                disabled: '{!allowEdit}',
+            },
+        },
+        {
+            xtype: 'radiofield',
+            name: 'cryptMode',
+            inputValue: 'upload',
+            boxLabel: gettext('Upload an existing client encryption key'),
+            padding: '0 0 0 25',
+            cbind: {
+                disabled: '{!isCreate}',
+            },
+            bind: {
+                disabled: '{!allowEdit}',
+            },
+            listeners: {
+                change: function (f, value) {
+                    let panel = this.up('inputpanel');
+                    if (!panel.rendered) {
+                        return;
+                    }
+                    let uploadKeyField = panel.down('field[name=crypt-key-upload]');
+                    uploadKeyField.setDisabled(!value);
+                    uploadKeyField.setHidden(!value);
+
+                    let uploadKeyButton = panel.down('filebutton[name=crypt-upload-button]');
+                    uploadKeyButton.setDisabled(!value);
+                    uploadKeyButton.setHidden(!value);
+
+                    if (value) {
+                        uploadKeyField.validate();
+                    } else {
+                        uploadKeyField.reset();
+                    }
+                },
+            },
+        },
+        {
+            xtype: 'fieldcontainer',
+            layout: 'hbox',
+            items: [
+                {
+                    xtype: 'proxmoxtextfield',
+                    name: 'crypt-key-upload',
+                    fieldLabel: gettext('Key'),
+                    value: '',
+                    disabled: true,
+                    hidden: true,
+                    allowBlank: false,
+                    labelAlign: 'right',
+                    flex: 1,
+                    emptyText: gettext('You can drag-and-drop a key file here.'),
+                    validator: function (value) {
+                        if (value.length) {
+                            let key;
+                            try {
+                                key = JSON.parse(value);
+                            } catch (e) {
+                                return 'Failed to parse key - ' + e;
+                            }
+                            if (key.data === undefined) {
+                                return 'Does not seems like a valid Proxmox Backup key!';
+                            }
+                        }
+                        return true;
+                    },
+                    afterRender: function () {
+                        if (!window.FileReader) {
+                            // No FileReader support in this browser
+                            return;
+                        }
+                        let cancel = function (ev) {
+                            ev = ev.event;
+                            if (ev.preventDefault) {
+                                ev.preventDefault();
+                            }
+                        };
+                        this.inputEl.on('dragover', cancel);
+                        this.inputEl.on('dragenter', cancel);
+                        this.inputEl.on('drop', (ev) => {
+                            cancel(ev);
+                            let files = ev.event.dataTransfer.files;
+                            Proxmox.Utils.loadTextFromFile(files[0], (v) => this.setValue(v));
+                        });
+                    },
+                },
+                {
+                    xtype: 'filebutton',
+                    name: 'crypt-upload-button',
+                    iconCls: 'fa fa-fw fa-folder-open-o x-btn-icon-el-default-toolbar-small',
+                    cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+                    margin: '0 0 0 4',
+                    disabled: true,
+                    hidden: true,
+                    listeners: {
+                        change: function (btn, e, value) {
+                            let ev = e.event;
+                            let field = btn.up().down('proxmoxtextfield[name=crypt-key-upload]');
+                            Proxmox.Utils.loadTextFromFile(ev.target.files[0], (v) =>
+                                field.setValue(v),
+                            );
+                            btn.reset();
+                        },
+                    },
+                },
+            ],
+        },
+        {
+            xtype: 'component',
+            border: false,
+            padding: '5 2',
+            userCls: 'pmx-hint',
+            html: // `<b style="color:red;font-weight:600;">${ngettext('Warning', 'Warnings', 1)}</b>: ` +
+                `<span class="fa fa-exclamation-triangle" style="color:red;font-size:14px;"></span> ` +
+                gettext(
+                    'Deleting or replacing the encryption key will break restoring backups created with it!',
+                ),
+            hidden: true,
+            bind: {
+                hidden: '{!showDangerousHint}',
+            },
+        },
+    ],
+});
 Ext.define('PMG.PBSInputPanel', {
     extend: 'Ext.tab.Panel',
     xtype: 'pmgPBSInputPanel',
@@ -173,6 +423,11 @@ Ext.define('PMG.PBSInputPanel', {
                 },
             ],
         },
+        {
+            xtype: 'pmgPBSEncryptionKeyTab',
+            title: gettext('Encryption'),
+            isCreate: this.isCreate,
+        },
     ],
 });
 
-- 
2.47.3





  parent reply	other threads:[~2026-06-03 18:05 UTC|newest]

Thread overview: 16+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-06-03 18:03 [PATCH pve-common/pmg-api/pmg-docs/pmg-gui 00/15] fix #3226: add support for encrypted backups Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pve-common 01/15] pbs-client: autogen key: rename old one if existing Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pve-common 02/15] pbs-client: add support for master public key Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pmg-api 03/15] api: pbs remote: fix delete_password invocation Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pmg-api 04/15] fix #3226: pbs backup: remote: add encryption key support Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pmg-api 05/15] pbs: job: add encrypted state to snapshot listing Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pmg-api 06/15] pbs: job: add verification " Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pmg-api 07/15] pmgbackup: add encypted and verification state to output Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pmg-api 08/15] api: pbs remote create/update: return parts of the configuration Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pmg-api 09/15] api: pmgbackup: add master-pubkey properties Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pmg-gui 10/15] pbs: snapshotview: add missing gettext invocations Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pmg-gui 11/15] utils: copy pbs helpers from pve-manager Stoiko Ivanov
2026-06-03 18:03 ` Stoiko Ivanov [this message]
2026-06-03 18:03 ` [PATCH pmg-gui 13/15] ui: pbs remote: allow to downloading/print new encryption key Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pmg-gui 14/15] ui: pbs snapshotview: add encryption and verification state Stoiko Ivanov
2026-06-03 18:03 ` [PATCH pmg-docs 15/15] pmgbackup: minimally document support for encrypted backups Stoiko Ivanov

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260603180445.98770-13-s.ivanov@proxmox.com \
    --to=s.ivanov@proxmox.com \
    --cc=pmg-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal