public inbox for pmg-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [PATCH pve-common/pmg-api/pmg-docs/pmg-gui 00/15] fix #3226: add support for encrypted backups
@ 2026-06-03 18:03 Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pve-common 01/15] pbs-client: autogen key: rename old one if existing Stoiko Ivanov
                   ` (14 more replies)
  0 siblings, 15 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

The following series adds support for adding an encryption-key to a pbs-remote
used for pmg backups. It aims for feature-parity with the encryption settings
and UX for pbs storages in pve.

The code was mostly copied from pve-storage and pve-manager, with minimal
adaptations.

I did some end to end tests and played around a bit with the UI and CLI:
* adding remotes with autogenerated keys
* deleting/replacing/uploading encryption keys to an existing remote
* adding a remote with provided master-pubkey, creating a backup and
  restoring it with proxmox-backup-client following the pbs-docs.

More testing would be very much appreciated.


pve-common:

Stoiko Ivanov (2):
  pbs-client: autogen key: rename old one if existing
  pbs-client: add support for master public key

 src/PVE/PBSClient.pm | 62 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 61 insertions(+), 1 deletion(-)


pmg-api:

Stoiko Ivanov (7):
  api: pbs remote: fix delete_password invocation
  fix #3226: pbs backup: remote: add encryption key support
  pbs: job: add encrypted state to snapshot listing
  pbs: job: add verification state to snapshot listing
  pmgbackup: add encypted and verification state to output
  api: pbs remote create/update: return parts of the configuration
  api: pmgbackup: add master-pubkey properties

 src/PMG/API2/PBS/Job.pm    |  38 ++++++++--
 src/PMG/API2/PBS/Remote.pm | 147 +++++++++++++++++++++++++++++++++++--
 src/PMG/CLI/pmgbackup.pm   |  24 +++++-
 src/PMG/PBSConfig.pm       |  12 +++
 4 files changed, 204 insertions(+), 17 deletions(-)


pmg-gui:

Stoiko Ivanov (5):
  pbs: snapshotview: add missing gettext invocations
  utils: copy pbs helpers from pve-manager
  fix #3326: ui: pbs remote: add encryption tab to edit window
  ui: pbs remote: allow to downloading/print new encryption key
  ui: pbs snapshotview: add encryption and verification state

 js/PBSRemoteEdit.js   | 466 ++++++++++++++++++++++++++++++++++++++++++
 js/PBSSnapshotView.js |  25 ++-
 js/Utils.js           |  44 ++++
 3 files changed, 529 insertions(+), 6 deletions(-)


pmg-docs:

Stoiko Ivanov (1):
  pmgbackup: minimally document support for encrypted backups

 pmgbackup.adoc | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)


Summary over all repositories:
  9 files changed, 810 insertions(+), 24 deletions(-)

-- 
Generated by murpp 0.12.0




^ permalink raw reply	[flat|nested] 16+ messages in thread

* [PATCH pve-common 01/15] pbs-client: autogen key: rename old one if existing
  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 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pve-common 02/15] pbs-client: add support for master public key Stoiko Ivanov
                   ` (13 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

while looking into adding encryption support for PMG, I noticed the
discrepancy between PVE::Storage::PBSPlugin and PVE::PBSClient

Semantically this follows:
478609d ("pbs: autogen key: rename old one if existing")

in pve-storage.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PVE/PBSClient.pm | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/PVE/PBSClient.pm b/src/PVE/PBSClient.pm
index 5af0d00..d8dd3f0 100644
--- a/src/PVE/PBSClient.pm
+++ b/src/PVE/PBSClient.pm
@@ -238,6 +238,9 @@ my sub run_client_cmd : prototype($$;$$$$) {
 sub autogen_encryption_key {
     my ($self) = @_;
     my $encfile = $self->encryption_key_file_name();
+    if (-f $encfile) {
+        rename $encfile, "$encfile.old";
+    }
     run_command(
         ['proxmox-backup-client', 'key', 'create', '--kdf', 'none', $encfile],
         errmsg => 'failed to create encryption key',
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pve-common 02/15] pbs-client: add support for master public key
  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 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pmg-api 03/15] api: pbs remote: fix delete_password invocation Stoiko Ivanov
                   ` (12 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

adapted from PVE::Storage::PBSPlugin
originally introduced there with
c56f7a7 ("pbs: allow setting up a master key")

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PVE/PBSClient.pm | 59 +++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 58 insertions(+), 1 deletion(-)

diff --git a/src/PVE/PBSClient.pm b/src/PVE/PBSClient.pm
index d8dd3f0..34ce843 100644
--- a/src/PVE/PBSClient.pm
+++ b/src/PVE/PBSClient.pm
@@ -132,6 +132,51 @@ my sub open_encryption_key {
     return $keyfd;
 }
 
+sub master_pubkey_file_name {
+    my ($self) = @_;
+
+    return "$self->{secret_dir}/$self->{storeid}.master.pem";
+}
+
+sub set_master_pubkey {
+    my ($self, $key) = @_;
+
+    my $master_pubkey_file = $self->master_pubkey_file_name();
+    mkdir($self->{secret_dir});
+
+    PVE::Tools::file_set_contents($master_pubkey_file, "$key\n", 0600);
+}
+
+sub delete_master_pubkey {
+    my ($self) = @_;
+
+    my $master_pubkey_file = $self->master_pubkey_file_name();
+
+    if (!unlink($master_pubkey_file)) {
+        return if $! == ENOENT;
+        die "failed to delete the master public key! $!\n";
+    }
+}
+
+# Returns a file handle if there is a master key, or `undef` if there is not. Dies on error.
+sub open_master_pubkey {
+    my ($self) = @_;
+
+    my $master_pubkey_file = $self->master_pubkey_file_name();
+
+    my $keyfd;
+    if (!open($keyfd, '<', $master_pubkey_file)) {
+        if ($! == ENOENT) {
+            die "master public key configured but no key file found!\n"
+                if $self->{'master-pubkey'};
+            return undef;
+        }
+        die "failed to open master public key: $master_pubkey_file: $!\n";
+    }
+
+    return $keyfd;
+}
+
 my $USE_CRYPT_PARAMS = {
     'proxmox-backup-client' => {
         backup => 1,
@@ -144,11 +189,16 @@ my $USE_CRYPT_PARAMS = {
     },
 };
 
+my $USE_MASTER_KEY = {
+    backup => 1,
+};
+
 my sub do_raw_client_cmd {
     my ($self, $client_cmd, $param, %opts) = @_;
 
     my $client_bin = delete($opts{binary}) || 'proxmox-backup-client';
     my $use_crypto = $USE_CRYPT_PARAMS->{$client_bin}->{$client_cmd} // 0;
+    my $use_master = $USE_MASTER_KEY->{$client_cmd};
 
     my $client_exe = "/usr/bin/$client_bin";
     die "executable not found '$client_exe'! $client_bin not installed?\n" if !-x $client_exe;
@@ -165,7 +215,7 @@ my sub do_raw_client_cmd {
     push(@$cmd, $client_exe, $client_cmd);
 
     # This must live in the top scope to not get closed before the `run_command`
-    my $keyfd;
+    my ($keyfd, $master_fd);
     if ($use_crypto) {
         if (defined($keyfd = open_encryption_key($self))) {
             my $flags = fcntl($keyfd, F_GETFD, 0)
@@ -173,6 +223,13 @@ my sub do_raw_client_cmd {
             fcntl($keyfd, F_SETFD, $flags & ~FD_CLOEXEC)
                 or die "failed to remove FD_CLOEXEC from encryption key file descriptor\n";
             push(@$cmd, '--crypt-mode=encrypt', '--keyfd=' . fileno($keyfd));
+            if ($use_master && defined($master_fd = $self->open_master_pubkey())) {
+                my $flags = fcntl($master_fd, F_GETFD, 0)
+                    // die "failed to get file descriptor flags: $!\n";
+                fcntl($master_fd, F_SETFD, $flags & ~FD_CLOEXEC)
+                    or die "failed to remove FD_CLOEXEC from master public key file descriptor\n";
+                push @$cmd, '--master-pubkey-fd=' . fileno($master_fd);
+            }
         } else {
             push(@$cmd, '--crypt-mode=none');
         }
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-api 03/15] api: pbs remote: fix delete_password invocation
  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 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pmg-api 04/15] fix #3226: pbs backup: remote: add encryption key support Stoiko Ivanov
                   ` (11 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

PVE::PBSClient already has the remote-id - so this parameter is not
needed/confusing.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/API2/PBS/Remote.pm | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/PMG/API2/PBS/Remote.pm b/src/PMG/API2/PBS/Remote.pm
index 561b4052..e5d63e68 100644
--- a/src/PMG/API2/PBS/Remote.pm
+++ b/src/PMG/API2/PBS/Remote.pm
@@ -216,7 +216,7 @@ __PACKAGE__->register_method({
             die "PBS remote '$remote' does not exist\n" if !$ids->{$remote};
 
             my $pbs = PVE::PBSClient->new($ids->{$remote}, $remote, $conf->{secret_dir});
-            $pbs->delete_password($remote);
+            $pbs->delete_password();
             delete $ids->{$remote};
 
             $conf->write();
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-api 04/15] fix #3226: pbs backup: remote: add encryption key support
  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
                   ` (2 preceding siblings ...)
  2026-06-03 18:03 ` [PATCH pmg-api 03/15] api: pbs remote: fix delete_password invocation Stoiko Ivanov
@ 2026-06-03 18:03 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pmg-api 05/15] pbs: job: add encrypted state to snapshot listing Stoiko Ivanov
                   ` (10 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

semantically this is copied from pve-storage while using
PVE::PBSClient.

tested with `pmgbackup proxmox-backup remote`

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/API2/PBS/Remote.pm | 46 ++++++++++++++++++++++++++++++++++++++
 src/PMG/PBSConfig.pm       |  6 +++++
 2 files changed, 52 insertions(+)

diff --git a/src/PMG/API2/PBS/Remote.pm b/src/PMG/API2/PBS/Remote.pm
index e5d63e68..881ab127 100644
--- a/src/PMG/API2/PBS/Remote.pm
+++ b/src/PMG/API2/PBS/Remote.pm
@@ -3,6 +3,8 @@ package PMG::API2::PBS::Remote;
 use strict;
 use warnings;
 
+use JSON;
+
 use PVE::SafeSyslog;
 use PVE::Tools qw(extract_param);
 use PVE::JSONSchema qw(get_standard_option);
@@ -84,6 +86,26 @@ __PACKAGE__->register_method({
             my $pbs = PVE::PBSClient->new($remotecfg, $remote, $conf->{secret_dir});
             $pbs->set_password($password) if defined($password);
 
+            my $encryption_key = extract_param($remotecfg, 'encryption-key');
+
+            if (defined($encryption_key)) {
+                my $decoded_key;
+                if ($encryption_key eq 'autogen') {
+                    $encryption_key = $pbs->autogen_encryption_key();
+                    $decoded_key = decode_json($encryption_key);
+                } else {
+                    $decoded_key = eval { decode_json($encryption_key) };
+                    if ($@ || !exists($decoded_key->{data})) {
+                        die
+                            "Value does not seems like a valid, JSON formatted encryption key!\n";
+                    }
+                    $pbs->set_encryption_key($encryption_key);
+                }
+                $remotecfg->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
+            } else {
+                $pbs->delete_encryption_key();
+            }
+
             $ids->{$remote} = $remotecfg;
             $conf->write();
         };
@@ -164,6 +186,9 @@ __PACKAGE__->register_method({
                 if ($opt eq 'password') {
                     $pbs->delete_password();
                 }
+                if ($opt eq 'encryption-key') {
+                    $pbs->delete_encryption_key();
+                }
                 delete $ids->{$remote}->{$opt};
             }
 
@@ -171,6 +196,26 @@ __PACKAGE__->register_method({
                 $pbs->set_password($password);
             }
 
+            if (exists($param->{'encryption-key'})) {
+                if (defined(my $encryption_key = extract_param($param, 'encryption-key'))) {
+                    my $decoded_key;
+                    if ($encryption_key eq 'autogen') {
+                        $encryption_key = $pbs->autogen_encryption_key();
+                        $decoded_key = decode_json($encryption_key);
+                    } else {
+                        $decoded_key = eval { decode_json($encryption_key) };
+                        if ($@ || !exists($decoded_key->{data})) {
+                            die
+                                "Value does not seems like a valid, JSON formatted encryption key!\n";
+                        }
+                        $pbs->set_encryption_key($encryption_key);
+                    }
+                    $param->{'encryption-key'} = $decoded_key->{fingerprint} || 1;
+                } else {
+                    $pbs->delete_encryption_key();
+                }
+            }
+
             my $remoteconfig = PMG::PBSConfig->check_config($remote, $param, 0, 1);
 
             foreach my $p (keys %$remoteconfig) {
@@ -217,6 +262,7 @@ __PACKAGE__->register_method({
 
             my $pbs = PVE::PBSClient->new($ids->{$remote}, $remote, $conf->{secret_dir});
             $pbs->delete_password();
+            $pbs->delete_encryption_key();
             delete $ids->{$remote};
 
             $conf->write();
diff --git a/src/PMG/PBSConfig.pm b/src/PMG/PBSConfig.pm
index 8498893c..4ceb81a3 100644
--- a/src/PMG/PBSConfig.pm
+++ b/src/PMG/PBSConfig.pm
@@ -125,6 +125,11 @@ sub properties {
             type => 'boolean',
             optional => 1,
         },
+        'encryption-key' => {
+            description =>
+                "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
+            type => 'string',
+        },
         %prune_properties,
     };
 }
@@ -147,6 +152,7 @@ sub options {
         'keep-weekly' => { optional => 1 },
         'keep-monthly' => { optional => 1 },
         'keep-yearly' => { optional => 1 },
+        'encryption-key' => { optional => 1 },
     };
 }
 
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-api 05/15] pbs: job: add encrypted state to snapshot listing
  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
                   ` (3 preceding siblings ...)
  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 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pmg-api 06/15] pbs: job: add verification " Stoiko Ivanov
                   ` (9 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

follows pve-storage commit:
878fe01 ("api: content: pass encrypted status for PBS backups")

the fallback to the 'crypt-mode' of the individual files was not
added, as this was a backward compat fix before
https://bugzilla.proxmox.com/show_bug.cgi?id=3139
was fixed in 2022/PBS 1.0.3.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/API2/PBS/Job.pm | 22 +++++++++++++++-------
 1 file changed, 15 insertions(+), 7 deletions(-)

diff --git a/src/PMG/API2/PBS/Job.pm b/src/PMG/API2/PBS/Job.pm
index a2233456..a4410d59 100644
--- a/src/PMG/API2/PBS/Job.pm
+++ b/src/PMG/API2/PBS/Job.pm
@@ -116,14 +116,16 @@ my sub get_snapshots {
         next if (scalar(@pxar) != 1);
 
         my $time_rfc3339 = strftime("%FT%TZ", gmtime($time));
+        my $res_item = {
+            'backup-id' => $id,
+            'backup-time' => $time_rfc3339,
+            ctime => $time,
+            size => $item->{size} // 1,
+        };
 
-        push @$res,
-            {
-                'backup-id' => $id,
-                'backup-time' => $time_rfc3339,
-                ctime => $time,
-                size => $item->{size} // 1,
-            };
+        $res_item->{encrypted} = $item->{fingerprint} if defined($item->{fingerprint});
+
+        push @$res, $res_item;
     }
     return $res;
 }
@@ -156,6 +158,12 @@ __PACKAGE__->register_method({
                 'backup-id' => { type => 'string' },
                 ctime => { type => 'string' },
                 size => { type => 'integer' },
+                encrypted => {
+                    description =>
+                        "If the backup is encrypted the value is the encryption-key fingerprint",
+                    type => 'string',
+                    optional => 1,
+                },
             },
         },
         links => [{ rel => 'child', href => "{backup-id}" }],
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-api 06/15] pbs: job: add verification state to snapshot listing
  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
                   ` (4 preceding siblings ...)
  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 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pmg-api 07/15] pmgbackup: add encypted and verification state to output Stoiko Ivanov
                   ` (8 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

follows pve-storage commit
9778e5c ("api: content listing: add comment and verification fields")

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/API2/PBS/Job.pm | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/src/PMG/API2/PBS/Job.pm b/src/PMG/API2/PBS/Job.pm
index a4410d59..636d3589 100644
--- a/src/PMG/API2/PBS/Job.pm
+++ b/src/PMG/API2/PBS/Job.pm
@@ -124,6 +124,7 @@ my sub get_snapshots {
         };
 
         $res_item->{encrypted} = $item->{fingerprint} if defined($item->{fingerprint});
+        $res_item->{verification} = $item->{verification} if defined($item->{verification});
 
         push @$res, $res_item;
     }
@@ -164,6 +165,21 @@ __PACKAGE__->register_method({
                     type => 'string',
                     optional => 1,
                 },
+                verification => {
+                    description => "Backup verification result",
+                    type => 'object',
+                    properties => {
+                        state => {
+                            description => "Backup verification state.",
+                            type => 'string',
+                        },
+                        upid => {
+                            description => "Backup verification UPID.",
+                            type => 'string',
+                        },
+                    },
+                    optional => 1,
+                },
             },
         },
         links => [{ rel => 'child', href => "{backup-id}" }],
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-api 07/15] pmgbackup: add encypted and verification state to output
  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
                   ` (5 preceding siblings ...)
  2026-06-03 18:03 ` [PATCH pmg-api 06/15] pbs: job: add verification " Stoiko Ivanov
@ 2026-06-03 18:03 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pmg-api 08/15] api: pbs remote create/update: return parts of the configuration Stoiko Ivanov
                   ` (7 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

print only the state of the verification result, and yes/no for
encryption instead of the fingerprint.

the rework of the output sub is inspired by
pve-ha-manager/src/PVE/CLI/ha_manager.pm

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/CLI/pmgbackup.pm | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/PMG/CLI/pmgbackup.pm b/src/PMG/CLI/pmgbackup.pm
index 11dd5672..9ef0c3c7 100644
--- a/src/PMG/CLI/pmgbackup.pm
+++ b/src/PMG/CLI/pmgbackup.pm
@@ -114,8 +114,15 @@ our $cmddef = {
             { node => $nodename },
             sub {
                 my ($data, $schema, $options) = @_;
+                my $props_to_print =
+                    ['backup-id', 'backup-time', 'size', 'encrypted', 'verify-state'];
+                for my $snapshot (@$data) {
+                    $snapshot->{encrypted} = $snapshot->{encrypted} ? "yes" : "no";
+                    $snapshot->{'verify-state'} = $snapshot->{verification}->{state};
+                }
+
                 PVE::CLIFormatter::print_api_result(
-                    $data, $schema, ['backup-id', 'backup-time', 'size'], $options,
+                    $data, $schema, $props_to_print, $options,
                 );
             },
             $PVE::RESTHandler::standard_output_options,
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-api 08/15] api: pbs remote create/update: return parts of the configuration
  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
                   ` (6 preceding siblings ...)
  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 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pmg-api 09/15] api: pmgbackup: add master-pubkey properties Stoiko Ivanov
                   ` (6 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

This follows pve-storage commit:
cd69ced ("api: storage create/update: return parts of the configuration")

returning the encryption-key upon creation, to offer it for
download/copying to the user in the GUI.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/API2/PBS/Remote.pm | 75 ++++++++++++++++++++++++++++++++++----
 1 file changed, 67 insertions(+), 8 deletions(-)

diff --git a/src/PMG/API2/PBS/Remote.pm b/src/PMG/API2/PBS/Remote.pm
index 881ab127..b5b9c3ad 100644
--- a/src/PMG/API2/PBS/Remote.pm
+++ b/src/PMG/API2/PBS/Remote.pm
@@ -67,16 +67,39 @@ __PACKAGE__->register_method({
     proxyto => 'master',
     protected => 1,
     parameters => PMG::PBSConfig->createSchema(1),
-    returns => { type => 'null' },
+    returns => {
+        type => 'object',
+        properties => {
+            remote => {
+                description => "The ID of the created PBS remote.",
+                type => 'string',
+            },
+            config => {
+                description => "Partial, possibly server generated, configuration properties.",
+                type => 'object',
+                optional => 1,
+                additionalProperties => 1,
+                properties => {
+                    'encryption-key' => {
+                        description => "The, possibly auto-generated, encryption-key.",
+                        optional => 1,
+                        type => 'string',
+                    },
+                },
+            },
+        },
+    },
     code => sub {
         my ($param) = @_;
 
+        my $remote;
+        my $encryption_key;
         my $code = sub {
             my $conf = PMG::PBSConfig->new();
             $conf->{ids} //= {};
             my $ids = $conf->{ids};
 
-            my $remote = extract_param($param, 'remote');
+            $remote = extract_param($param, 'remote');
             die "PBS remote '$remote' already exists\n" if $ids->{$remote};
 
             my $remotecfg = PMG::PBSConfig->check_config($remote, $param, 1);
@@ -86,7 +109,7 @@ __PACKAGE__->register_method({
             my $pbs = PVE::PBSClient->new($remotecfg, $remote, $conf->{secret_dir});
             $pbs->set_password($password) if defined($password);
 
-            my $encryption_key = extract_param($remotecfg, 'encryption-key');
+            $encryption_key = extract_param($remotecfg, 'encryption-key');
 
             if (defined($encryption_key)) {
                 my $decoded_key;
@@ -111,8 +134,14 @@ __PACKAGE__->register_method({
         };
 
         PMG::PBSConfig::lock_config($code, "add PBS remote failed");
+        my $res = {
+            remote => $remote,
+            config => {
+                'encryption-key' => $encryption_key,
+            },
+        };
 
-        return undef;
+        return $res;
     },
 });
 
@@ -162,10 +191,33 @@ __PACKAGE__->register_method({
     protected => 1,
     proxyto => 'master',
     parameters => PMG::PBSConfig->updateSchema(),
-    returns => { type => 'null' },
+    returns => {
+        type => 'object',
+        properties => {
+            remote => {
+                description => "The ID of the created PBS remote.",
+                type => 'string',
+            },
+            config => {
+                description => "Partial, possibly server generated, configuration properties.",
+                type => 'object',
+                optional => 1,
+                additionalProperties => 1,
+                properties => {
+                    'encryption-key' => {
+                        description => "The, possibly auto-generated, encryption-key.",
+                        optional => 1,
+                        type => 'string',
+                    },
+                },
+            },
+        },
+    },
     code => sub {
         my ($param) = @_;
 
+        my $remote;
+        my $encryption_key;
         my $code = sub {
 
             my $conf = PMG::PBSConfig->new();
@@ -174,7 +226,7 @@ __PACKAGE__->register_method({
             my $digest = extract_param($param, 'digest');
             PVE::SectionConfig::assert_if_modified($conf, $digest);
 
-            my $remote = extract_param($param, 'remote');
+            $remote = extract_param($param, 'remote');
 
             die "PBS remote '$remote' does not exist\n" if !$ids->{$remote};
 
@@ -197,7 +249,7 @@ __PACKAGE__->register_method({
             }
 
             if (exists($param->{'encryption-key'})) {
-                if (defined(my $encryption_key = extract_param($param, 'encryption-key'))) {
+                if (defined($encryption_key = extract_param($param, 'encryption-key'))) {
                     my $decoded_key;
                     if ($encryption_key eq 'autogen') {
                         $encryption_key = $pbs->autogen_encryption_key();
@@ -227,7 +279,14 @@ __PACKAGE__->register_method({
 
         PMG::PBSConfig::lock_config($code, "update PBS remote failed");
 
-        return undef;
+        my $res = {
+            remote => $remote,
+            config => {
+                'encryption-key' => $encryption_key,
+            },
+        };
+
+        return $res;
     },
 });
 
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-api 09/15] api: pmgbackup: add master-pubkey properties
  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
                   ` (7 preceding siblings ...)
  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 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pmg-gui 10/15] pbs: snapshotview: add missing gettext invocations Stoiko Ivanov
                   ` (5 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

adapted from pve-storage commit
 c56f7a7 ("pbs: allow setting up a master key")

the actual invocation of proxmox-backup-client with the master-key
needs a versioned dependency bump on pve-common.

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 src/PMG/API2/PBS/Remote.pm | 28 ++++++++++++++++++++++++++++
 src/PMG/CLI/pmgbackup.pm   | 15 +++++++++++++--
 src/PMG/PBSConfig.pm       |  6 ++++++
 3 files changed, 47 insertions(+), 2 deletions(-)

diff --git a/src/PMG/API2/PBS/Remote.pm b/src/PMG/API2/PBS/Remote.pm
index b5b9c3ad..397d802b 100644
--- a/src/PMG/API2/PBS/Remote.pm
+++ b/src/PMG/API2/PBS/Remote.pm
@@ -4,6 +4,7 @@ use strict;
 use warnings;
 
 use JSON;
+use MIME::Base64 qw(decode_base64);
 
 use PVE::SafeSyslog;
 use PVE::Tools qw(extract_param);
@@ -102,6 +103,7 @@ __PACKAGE__->register_method({
             $remote = extract_param($param, 'remote');
             die "PBS remote '$remote' already exists\n" if $ids->{$remote};
 
+            my $master_key = extract_param($param, 'master-pubkey');
             my $remotecfg = PMG::PBSConfig->check_config($remote, $param, 1);
 
             my $password = extract_param($remotecfg, 'password');
@@ -129,6 +131,17 @@ __PACKAGE__->register_method({
                 $pbs->delete_encryption_key();
             }
 
+            if (defined($master_key)) {
+                die "'master-pubkey' can only be used together with 'encryption-key'\n"
+                    if !defined($remotecfg->{'encryption-key'});
+
+                my $decoded = decode_base64($master_key);
+                $pbs->set_master_pubkey($decoded);
+                $remotecfg->{'master-pubkey'} = 1;
+            } else {
+                $pbs->delete_master_pubkey();
+            }
+
             $ids->{$remote} = $remotecfg;
             $conf->write();
         };
@@ -241,6 +254,9 @@ __PACKAGE__->register_method({
                 if ($opt eq 'encryption-key') {
                     $pbs->delete_encryption_key();
                 }
+                if ($opt eq 'master-pubkey') {
+                    $pbs->delete_master_pubkey();
+                }
                 delete $ids->{$remote}->{$opt};
             }
 
@@ -268,6 +284,17 @@ __PACKAGE__->register_method({
                 }
             }
 
+            if (exists($param->{'master-pubkey'})) {
+                if (defined(my $master_key = extract_param($param, 'master-pubkey'))) {
+                    my $decoded = decode_base64($master_key);
+
+                    $pbs->set_master_pubkey($decoded);
+                    $param->{'master-pubkey'} = 1;
+                } else {
+                    $pbs->delete_master_pubkey();
+                }
+            }
+
             my $remoteconfig = PMG::PBSConfig->check_config($remote, $param, 0, 1);
 
             foreach my $p (keys %$remoteconfig) {
@@ -322,6 +349,7 @@ __PACKAGE__->register_method({
             my $pbs = PVE::PBSClient->new($ids->{$remote}, $remote, $conf->{secret_dir});
             $pbs->delete_password();
             $pbs->delete_encryption_key();
+            $pbs->delete_master_pubkey();
             delete $ids->{$remote};
 
             $conf->write();
diff --git a/src/PMG/CLI/pmgbackup.pm b/src/PMG/CLI/pmgbackup.pm
index 9ef0c3c7..43428ef2 100644
--- a/src/PMG/CLI/pmgbackup.pm
+++ b/src/PMG/CLI/pmgbackup.pm
@@ -3,6 +3,8 @@ package PMG::CLI::pmgbackup;
 use strict;
 use warnings;
 
+use MIME::Base64 qw(encode_base64);
+
 use PVE::Tools;
 use PVE::SafeSyslog;
 use PVE::INotify;
@@ -43,9 +45,18 @@ sub param_mapping {
         },
     };
 
+    my $master_key_map = {
+        name => 'master-pubkey',
+        desc => 'a file containing a PEM-formatted master public key',
+        func => sub {
+            my ($value) = @_;
+            return encode_base64(PVE::Tools::file_get_contents($value), '');
+        },
+    };
+
     my $mapping = {
-        'create' => [$password_map, $enc_key_map],
-        'update_config' => [$password_map, $enc_key_map],
+        'create' => [$password_map, $enc_key_map, $master_key_map],
+        'update_config' => [$password_map, $enc_key_map, $master_key_map],
     };
     return $mapping->{$name};
 }
diff --git a/src/PMG/PBSConfig.pm b/src/PMG/PBSConfig.pm
index 4ceb81a3..ec4b5405 100644
--- a/src/PMG/PBSConfig.pm
+++ b/src/PMG/PBSConfig.pm
@@ -130,6 +130,11 @@ sub properties {
                 "Encryption key. Use 'autogen' to generate one automatically without passphrase.",
             type => 'string',
         },
+        'master-pubkey' => {
+            description =>
+                "Base64-encoded, PEM-formatted public RSA key. Used to encrypt a copy of the encryption-key which will be added to each encrypted backup.",
+            type => 'string',
+        },
         %prune_properties,
     };
 }
@@ -153,6 +158,7 @@ sub options {
         'keep-monthly' => { optional => 1 },
         'keep-yearly' => { optional => 1 },
         'encryption-key' => { optional => 1 },
+        'master-pubkey' => { optional => 1 },
     };
 }
 
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-gui 10/15] pbs: snapshotview: add missing gettext invocations
  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
                   ` (8 preceding siblings ...)
  2026-06-03 18:03 ` [PATCH pmg-api 09/15] api: pmgbackup: add master-pubkey properties Stoiko Ivanov
@ 2026-06-03 18:03 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pmg-gui 11/15] utils: copy pbs helpers from pve-manager Stoiko Ivanov
                   ` (4 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

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

diff --git a/js/PBSSnapshotView.js b/js/PBSSnapshotView.js
index f3fe475..82f0f96 100644
--- a/js/PBSSnapshotView.js
+++ b/js/PBSSnapshotView.js
@@ -252,17 +252,17 @@ Ext.define('PMG.PBSConfig', {
             },
             columns: [
                 {
-                    text: 'Group ID',
+                    text: gettext('Group ID'),
                     dataIndex: 'backup-id',
                     flex: 1,
                 },
                 {
-                    text: 'Time',
+                    text: gettext('Time'),
                     dataIndex: 'backup-time',
                     width: 180,
                 },
                 {
-                    text: 'Size',
+                    text: gettext('Size'),
                     dataIndex: 'size',
                     renderer: Proxmox.Utils.render_size,
                     flex: 1,
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-gui 11/15] utils: copy pbs helpers from pve-manager
  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
                   ` (9 preceding siblings ...)
  2026-06-03 18:03 ` [PATCH pmg-gui 10/15] pbs: snapshotview: add missing gettext invocations Stoiko Ivanov
@ 2026-06-03 18:03 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pmg-gui 12/15] fix #3326: ui: pbs remote: add encryption tab to edit window Stoiko Ivanov
                   ` (3 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

as we'll reuse the widgets from pve-manager for adding pbs encryption
support to PMG, copy the helpers from pve-manager.

taken from pve-manager commits:
fdde857a ("fix #4393: ui: storage backup view: make pbs-specific columns sortable")
957a53bd ("ui: add comment/verification columns to backup/content grid")
3003a59d ("ui: guest backup view: add encrypted column for PBS storages")
14ba33fb ("ui: storage: pbs: improve encryption key handling")

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

diff --git a/js/Utils.js b/js/Utils.js
index 3332f9b..0c3032f 100644
--- a/js/Utils.js
+++ b/js/Utils.js
@@ -910,6 +910,50 @@ Ext.define('PMG.Utils', {
             saupdate: ['', gettext('SpamAssassin update')],
         });
     },
+
+    render_pbs_fingerprint: (fp) => fp.substring(0, 23),
+
+    render_backup_encryption: function (v, meta, record) {
+        if (!v) {
+            return gettext('No');
+        }
+        let tip = '';
+        if (v.match(/^[a-fA-F0-9]{2}:/)) {
+            // fingerprint
+            tip = `Key fingerprint ${PMG.Utils.render_pbs_fingerprint(v)}`;
+        }
+        let icon = `<i class="fa fa-fw fa-lock good"></i>`;
+        return `<span data-qtip="${tip}">${icon} ${gettext('Encrypted')}</span>`;
+    },
+
+    render_backup_verification: function (v, meta, record) {
+        let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`;
+        if (v === undefined || v === null) {
+            return i('question-circle-o warning', gettext('None'));
+        }
+        let tip = '';
+        let txt = gettext('Failed');
+        let iconCls = 'times critical';
+        if (v.state === 'ok') {
+            txt = gettext('OK');
+            iconCls = 'check good';
+            let now = Date.now() / 1000;
+            let task = Proxmox.Utils.parse_task_upid(v.upid);
+            let verify_time = Proxmox.Utils.render_timestamp(task.starttime);
+            tip = `Last verify task started on ${verify_time}`;
+            if (now - v.starttime > 30 * 24 * 60 * 60) {
+                tip = `Last verify task over 30 days ago: ${verify_time}`;
+                iconCls = 'check warning';
+            }
+        }
+        return `<span data-qtip="${tip}"> ${i(iconCls, txt)} </span>`;
+    },
+    verificationStateOrder: {
+        failed: 0,
+        none: 1,
+        ok: 2,
+        __default__: 3,
+    },
 });
 
 Ext.define('PMG.Async', {
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-gui 12/15] fix #3326: ui: pbs remote: add encryption tab to edit window
  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
                   ` (10 preceding siblings ...)
  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
  2026-06-03 18:03 ` [PATCH pmg-gui 13/15] ui: pbs remote: allow to downloading/print new encryption key Stoiko Ivanov
                   ` (2 subsequent siblings)
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

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





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-gui 13/15] ui: pbs remote: allow to downloading/print new encryption key
  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
                   ` (11 preceding siblings ...)
  2026-06-03 18:03 ` [PATCH pmg-gui 12/15] fix #3326: ui: pbs remote: add encryption tab to edit window Stoiko Ivanov
@ 2026-06-03 18:03 ` 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
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

taken from pve-manager/www/manager6/storage/PBSEdit.js
introduced there in commit
d1a7c6ee ("ui: storage/PBS: allow to download/print new encryption key")

minimally adapted (rename storage-id/sid to remote-id/rid,
renaming of utils class).

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

diff --git a/js/PBSRemoteEdit.js b/js/PBSRemoteEdit.js
index aa51184..dfcf9a8 100644
--- a/js/PBSRemoteEdit.js
+++ b/js/PBSRemoteEdit.js
@@ -1,3 +1,201 @@
+Ext.define('PMG.PBSKeyShow', {
+    extend: 'Ext.window.Window',
+    xtype: 'pmgPBSKeyShow',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    width: 600,
+    modal: true,
+    resizable: false,
+    title: gettext('Important: Save your Encryption Key'),
+
+    // avoid that esc closes this by mistake, force user to more manual action
+    onEsc: Ext.emptyFn,
+    closable: false,
+
+    items: [
+        {
+            xtype: 'form',
+            layout: {
+                type: 'vbox',
+                align: 'stretch',
+            },
+            bodyPadding: 10,
+            border: false,
+            defaults: {
+                anchor: '100%',
+                border: false,
+                padding: '10 0 0 0',
+            },
+            items: [
+                {
+                    xtype: 'textfield',
+                    fieldLabel: gettext('Key'),
+                    labelWidth: 80,
+                    inputId: 'encryption-key-value',
+                    cbind: {
+                        value: '{key}',
+                    },
+                    editable: false,
+                },
+                {
+                    xtype: 'component',
+                    html:
+                        gettext(
+                            'Keep your encryption key safe, but easily accessible for disaster recovery.',
+                        ) +
+                        '<br>' +
+                        gettext('We recommend the following safe-keeping strategy:'),
+                },
+                {
+                    xtyp: 'container',
+                    layout: 'hbox',
+                    items: [
+                        {
+                            xtype: 'component',
+                            html: '1. ' + gettext('Save the key in your password manager.'),
+                            flex: 1,
+                        },
+                        {
+                            xtype: 'button',
+                            text: gettext('Copy Key'),
+                            iconCls: 'fa fa-clipboard x-btn-icon-el-default-toolbar-small',
+                            cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+                            width: 110,
+                            handler: function (b) {
+                                document.getElementById('encryption-key-value').select();
+                                document.execCommand('copy');
+                            },
+                        },
+                    ],
+                },
+                {
+                    xtype: 'container',
+                    layout: 'hbox',
+                    items: [
+                        {
+                            xtype: 'component',
+                            html:
+                                '2. ' +
+                                gettext(
+                                    'Download the key to a USB (pen) drive, placed in secure vault.',
+                                ),
+                            flex: 1,
+                        },
+                        {
+                            xtype: 'button',
+                            text: gettext('Download'),
+                            iconCls: 'fa fa-download x-btn-icon-el-default-toolbar-small',
+                            cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+                            width: 110,
+                            handler: function (b) {
+                                let win = this.up('window');
+
+                                let pmgID = Proxmox.NodeName || window.location.hostname;
+                                let name = `pmg-${pmgID}-remote-${win.rid}.enc`;
+
+                                let hiddenElement = document.createElement('a');
+                                hiddenElement.href = 'data:attachment/text,' + encodeURI(win.key);
+                                hiddenElement.target = '_blank';
+                                hiddenElement.download = name;
+                                hiddenElement.click();
+                            },
+                        },
+                    ],
+                },
+                {
+                    xtype: 'container',
+                    layout: 'hbox',
+                    items: [
+                        {
+                            xtype: 'component',
+                            html:
+                                '3. ' +
+                                gettext('Print as paperkey, laminated and placed in secure vault.'),
+                            flex: 1,
+                        },
+                        {
+                            xtype: 'button',
+                            text: gettext('Print Key'),
+                            iconCls: 'fa fa-print x-btn-icon-el-default-toolbar-small',
+                            cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+                            width: 110,
+                            handler: function (b) {
+                                let win = this.up('window');
+                                win.paperkey(win.key);
+                            },
+                        },
+                    ],
+                },
+            ],
+        },
+        {
+            xtype: 'component',
+            border: false,
+            padding: '10 10 10 10',
+            userCls: 'pmx-hint',
+            html: gettext(
+                'Please save the encryption key - losing it will render any backup created with it unusable',
+            ),
+        },
+    ],
+    buttons: [
+        {
+            text: gettext('Close'),
+            handler: function (b) {
+                let win = this.up('window');
+                win.close();
+            },
+        },
+    ],
+    paperkey: function (keyString) {
+        let me = this;
+
+        const key = JSON.parse(keyString);
+
+        const qrwidth = 500;
+        let qrdiv = document.createElement('div');
+        let qrcode = new QRCode(qrdiv, {
+            width: qrwidth,
+            height: qrwidth,
+            correctLevel: QRCode.CorrectLevel.H,
+        });
+        qrcode.makeCode(keyString);
+
+        let shortKeyFP = '';
+        if (key.fingerprint) {
+            shortKeyFP = PMG.Utils.render_pbs_fingerprint(key.fingerprint);
+        }
+
+        let printFrame = document.createElement('iframe');
+        Object.assign(printFrame.style, {
+            position: 'fixed',
+            right: '0',
+            bottom: '0',
+            width: '0',
+            height: '0',
+            border: '0',
+        });
+        const prettifiedKey = JSON.stringify(key, null, 2);
+        const keyQrBase64 = qrdiv.children[0].toDataURL('image/png');
+        const html = `<html><head><script>
+	    window.addEventListener('DOMContentLoaded', (ev) => window.print());
+	</script><style>@media print and (max-height: 150mm) {
+	  h4, p { margin: 0; font-size: 1em; }
+	}</style></head><body style="padding: 5px;">
+	<h4>Encryption Key - Remote '${me.rid}' (${shortKeyFP})</h4>
+<p style="font-size:1.2em;font-family:monospace;white-space:pre-wrap;overflow-wrap:break-word;">
+-----BEGIN PROXMOX BACKUP KEY-----
+${prettifiedKey}
+-----END PROXMOX BACKUP KEY-----</p>
+	<center><img style="width: 100%; max-width: ${qrwidth}px;" src="${keyQrBase64}"></center>
+	</body></html>`;
+
+        printFrame.src = 'data:text/html;base64,' + btoa(html);
+        document.body.appendChild(printFrame);
+        me.on('destroy', () => document.body.removeChild(printFrame));
+    },
+});
+
 Ext.define('PMG.panel.PBSEncryptionKeyTab', {
     extend: 'Proxmox.panel.InputPanel',
     xtype: 'pmgPBSEncryptionKeyTab',
@@ -441,6 +639,19 @@ Ext.define('PMG.PBSEdit', {
 
     bodyPadding: 0,
 
+    apiCallDone: function (success, response, options) {
+        let res = response.result.data;
+        if (!(res && res.config && res.config['encryption-key'])) {
+            return;
+        }
+        let key = res.config['encryption-key'];
+        Ext.create('PMG.PBSKeyShow', {
+            autoShow: true,
+            rid: res.remote,
+            key: key,
+        });
+    },
+
     initComponent: function () {
         let me = this;
 
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-gui 14/15] ui: pbs snapshotview: add encryption and verification state
  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
                   ` (12 preceding siblings ...)
  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 ` Stoiko Ivanov
  2026-06-03 18:03 ` [PATCH pmg-docs 15/15] pmgbackup: minimally document support for encrypted backups Stoiko Ivanov
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

follows pve-manager commit:
a4b87129 ("ui: storage content: add encryption and verification columns for PBS")

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 js/PBSSnapshotView.js | 19 ++++++++++++++++---
 1 file changed, 16 insertions(+), 3 deletions(-)

diff --git a/js/PBSSnapshotView.js b/js/PBSSnapshotView.js
index 82f0f96..774ad26 100644
--- a/js/PBSSnapshotView.js
+++ b/js/PBSSnapshotView.js
@@ -268,10 +268,23 @@ Ext.define('PMG.PBSConfig', {
                     flex: 1,
                 },
                 {
-                    text: 'Encrypted',
+                    text: gettext('Encrypted'),
                     dataIndex: 'encrypted',
-                    hidden: true, // FIXME: actually return from API
-                    renderer: Proxmox.Utils.format_boolean,
+                    renderer: PMG.Utils.render_backup_encryption,
+                    flex: 1,
+                },
+                {
+                    text: gettext('Verify State'),
+                    dataIndex: 'verification',
+                    renderer: PMG.Utils.render_backup_verification,
+                    sorter: {
+                        property: 'verification',
+                        transform: (value) => {
+                            let state = value?.state ?? 'none';
+                            let order = PMG.Utils.verificationStateOrder;
+                            return order[state] ?? order.__default__;
+                        },
+                    },
                     flex: 1,
                 },
             ],
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

* [PATCH pmg-docs 15/15] pmgbackup: minimally document support for encrypted backups
  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
                   ` (13 preceding siblings ...)
  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 ` Stoiko Ivanov
  14 siblings, 0 replies; 16+ messages in thread
From: Stoiko Ivanov @ 2026-06-03 18:03 UTC (permalink / raw)
  To: pmg-devel

Signed-off-by: Stoiko Ivanov <s.ivanov@proxmox.com>
---
 pmgbackup.adoc | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/pmgbackup.adoc b/pmgbackup.adoc
index e2da5ae..5ecabb2 100644
--- a/pmgbackup.adoc
+++ b/pmgbackup.adoc
@@ -132,6 +132,22 @@ Retype new password: ******
 The fingerprint is optional, if the certificate of the Proxmox Backup Server
 remote is signed by a CA trusted by {pmg}.
 
+You can also encrypt backups, by autogenerating or providing an encryption key
+for a Proxmox Backup Server remote.
+
+NOTE: Keep a copy of the symmetric encryption key in a secure place, or print
+it out. Without the encryption key will not be able to restore an encrypted
+backup. Alternatively you can add a master public key to a remote config, and
+have the symmetric encryption key, encrypted by the master key and added to
+each backup.
+
+.Example addition of a Proxmox Backup Server remote with autogenerated encryption key.
+----
+# pmgbackup proxmox-backup remote add shared-pbs --datastore public --server backup.cloud-provider.example --user 'pmgbackup@pbs!token' --password --encryption-key autogen
+Enter new password: ******
+Retype new password: ******
+----
+
 Additionally, you can configure `prune-settings` for each remote, giving you
 flexible control over how many backups should be stored on the Proxmox Backup
 Server over a specific period of time.
-- 
2.47.3





^ permalink raw reply related	[flat|nested] 16+ messages in thread

end of thread, other threads:[~2026-06-03 18:05 UTC | newest]

Thread overview: 16+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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 ` [PATCH pmg-gui 12/15] fix #3326: ui: pbs remote: add encryption tab to edit window Stoiko Ivanov
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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal