From: "Fabian Grünbichler" <f.gruenbichler@proxmox.com>
To: Christian Ebner <c.ebner@proxmox.com>, pbs-devel@lists.proxmox.com
Subject: Re: [PATCH proxmox-backup 2/3] sync: pull: refactor decryption key loading checks
Date: Fri, 24 Apr 2026 14:46:07 +0200 [thread overview]
Message-ID: <1777033255.umlrtuml3i.astroid@yuna.none> (raw)
In-Reply-To: <20260424103607.531400-3-c.ebner@proxmox.com>
On April 24, 2026 12:36 pm, Christian Ebner wrote:
> In an effort to improve code readability and maintanablity.
>
> By pulling out the logic into a dedicated helper, nested branches can
> be switched over to early return statements instead, greatly reducing
> the required nesting level.
>
> Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
> ---
> src/server/pull.rs | 225 +++++++++++++++++++++++++++------------------
> 1 file changed, 137 insertions(+), 88 deletions(-)
>
> diff --git a/src/server/pull.rs b/src/server/pull.rs
> index 051405ae7..d0f886cb5 100644
> --- a/src/server/pull.rs
> +++ b/src/server/pull.rs
> @@ -755,95 +755,28 @@ async fn pull_snapshot<'a>(
> return Ok(sync_stats);
> }
>
> - let mut crypt_config = None;
> - let mut new_manifest = None;
> - if let Some(key_fp) = manifest.fingerprint().with_context(|| prefix.clone())? {
> - // source got key fingerprint, expect contents to be signed or encrypted
> - if let Some((key_id, config)) = params
> - .crypt_configs
> - .iter()
> - .find(|(_id, crypt_conf)| crypt_conf.fingerprint() == *key_fp.bytes())
> - {
> - // check if source is encrypted or contents signed
> - if !manifest
> - .files()
> - .iter()
> - .all(|f| f.chunk_crypt_mode() == CryptMode::Encrypt)
> - {
> - log_sender
> - .log(
> - Level::WARN,
> - format!("Snapshot not fully encrypted, sync as is despite matching key '{key_id}' with fingerprint {key_fp}"),
> - )
> - .await?;
> - } else {
> - manifest
> - .check_signature(config)
> - .context("failed to check source manifest signature")
> - .with_context(|| prefix.clone())?;
> -
> - if let Some(existing_manifest) = existing_target_manifest {
> - if let Some(existing_fingerprint) = existing_manifest
> - .fingerprint()
> - .with_context(|| prefix.clone())?
> - {
> - if existing_fingerprint != key_fp {
> - bail!("Detected local encrypted or signed snapshot with encryption key mismatch!");
> - }
> - } else if let Some(source_fp) = manifest
> - .get_change_detection_fingerprint()
> - .context("failed to parse change detection fingerprint of source manifest")
> - .with_context(|| prefix.clone())?
> - {
> - // Stored fp is HMAC over the unencrypted source's protected fields; recompute
> - // over the locally decrypted manifest, not the fresh encrypted remote one.
> - let target_fp = existing_manifest
> - .signature(config)
> - .with_context(|| prefix.clone())?;
> - if target_fp == *source_fp.bytes() {
> - fetch_log().await?;
> - cleanup().await?;
> - return Ok(sync_stats); // nothing changed
> - }
> -
> - bail!("Change detection fingerprint mismatch, refuse to continue");
> - }
> - } else {
> - log_sender
> - .log(
> - Level::INFO,
> - format!("Found matching key '{key_id}' with fingerprint {key_fp}, decrypt on pull"),
> - )
> - .await?;
> - crypt_config = Some(Arc::clone(config));
> - new_manifest = Some(Arc::new(Mutex::new(BackupManifest::new(snapshot.into()))));
> - }
> - }
> - } else if let Some(existing_target_manifest) = existing_target_manifest {
> - if let Some(existing_fingerprint) = existing_target_manifest
> - .fingerprint()
> - .with_context(|| prefix.clone())?
> - {
> - if existing_fingerprint != key_fp {
> - // pre-existing local manifest for encrypted snapshot with key mismatch
> - bail!("Local encrypted or signed snapshot with different key detected, refuse to sync");
> - }
> - } else {
> - // pre-existing local manifest without key-fingerprint was previously decrypted,
> - // never overwrite with encrypted
> - bail!(
> - "local snapshot was previously decrypted but no matching decryption key is configured, refuse to sync"
> - );
> - }
> - } else if !params.crypt_configs.is_empty() {
> - log_sender
> - .log(
> - Level::INFO,
> - format!("{prefix}: No matching key found, sync without decryption"),
> - )
> - .await?;
> + let (crypt_config, new_manifest) = match optionally_use_decryption_key(
> + Arc::clone(¶ms),
> + &manifest,
> + existing_target_manifest.as_ref(),
> + prefix.clone(),
> + Arc::clone(&log_sender),
> + )
> + .await?
> + {
> + (None, false) => (None, None), // regular pull without decryption
> + (Some(crypt_config), false) => {
> + // decrypt while pull
> + let new_manifest = Arc::new(Mutex::new(BackupManifest::new(snapshot.into())));
> + (Some(crypt_config), Some(new_manifest))
> }
> - }
> + (_, true) => {
> + // nothing changed
> + fetch_log().await?;
> + cleanup().await?;
> + return Ok(sync_stats);
> + }
> + };
>
> for item in manifest.files() {
> let mut path = snapshot.full_path();
> @@ -979,6 +912,122 @@ async fn pull_snapshot<'a>(
> Ok(sync_stats)
> }
>
> +/// Check if the decryption key should be used to decrypt the snapshot during
> +/// pull based on given pull parameter, source and optionally already present
> +/// target manifest.
> +///
> +/// The boolean flag in the returned tuple indicates whether the pull can be
> +/// skipped altogether, since the already existing target is unchanged.
> +/// If decryption should happen, the matching decryption key is returned.
> +async fn optionally_use_decryption_key(
> + params: Arc<PullParameters>,
> + manifest: &BackupManifest,
> + existing_target_manifest: Option<&BackupManifest>,
> + prefix: String,
> + log_sender: Arc<LogLineSender>,
> +) -> Result<(Option<Arc<CryptConfig>>, bool), Error> {
> + let key_fp = match manifest.fingerprint().with_context(|| prefix.clone())? {
> + Some(key_fp) => key_fp,
> + None => return Ok((None, false)), // no fingerprint on source, regular pull
> + };
this can be a let else isntead
> +
> + // source got key fingerprint, expect contents to be signed or encrypted
> + let (key_id, config) = match params
> + .crypt_configs
> + .iter()
> + .find(|(_id, crypt_conf)| crypt_conf.fingerprint() == *key_fp.bytes())
> + {
> + Some(key_id_and_config) => key_id_and_config,
this as well
> + None => {
> + // no matching key found in list of configured ones
> + if let Some(existing_target_manifest) = existing_target_manifest {
> + if let Some(existing_fingerprint) = existing_target_manifest
> + .fingerprint()
> + .with_context(|| prefix.clone())?
> + {
> + if existing_fingerprint != key_fp {
> + // pre-existing local manifest for encrypted snapshot with key mismatch
> + bail!(
> + "Local encrypted or signed snapshot with different key detected, refuse to sync"
> + );
> + }
> + } else {
> + // pre-existing local manifest without key-fingerprint was previously decrypted,
> + // never overwrite with encrypted
> + bail!(
> + "local snapshot was previously decrypted but no matching decryption key is configured, refuse to sync"
> + );
> + }
> + } else if !params.crypt_configs.is_empty() {
> + log_sender
> + .log(
> + Level::INFO,
> + format!("{prefix}: No matching key found, sync without decryption"),
> + )
> + .await?;
> + }
> + return Ok((None, false));
> + }
> + };
> +
> + // check if source is encrypted or contents signed
> + if !manifest
> + .files()
> + .iter()
> + .all(|f| f.chunk_crypt_mode() == CryptMode::Encrypt)
> + {
> + log_sender
> + .log(
> + Level::WARN,
> + format!("Snapshot not fully encrypted, sync as is despite matching key '{key_id}' with fingerprint {key_fp}"),
> + )
> + .await?;
> + return Ok((None, false));
> + }
> +
> + manifest
> + .check_signature(config)
> + .context("failed to check source manifest signature")
> + .with_context(|| prefix.clone())?;
so at this point we are if
- the source is encrypted, we found a matching key, and the signature is
okay
> +
> + // avoid overwriting pre-existing target manifest
> + if let Some(existing_manifest) = existing_target_manifest {
> + if let Some(existing_fingerprint) = existing_manifest
> + .fingerprint()
> + .with_context(|| prefix.clone())?
> + {
> + if existing_fingerprint != key_fp {
> + bail!("Detected local encrypted or signed snapshot with encryption key mismatch!");
> + }
if it is signed we don't check the change detection fingerprint..
> + } else if let Some(source_fp) = manifest
> + .get_change_detection_fingerprint()
> + .context("failed to parse change detection fingerprint of source manifest")
> + .with_context(|| prefix.clone())?
> + {
> + // Stored fp is HMAC over the unencrypted source's protected fields; recompute
> + // over the locally decrypted manifest, not the fresh encrypted remote one.
> + let target_fp = existing_manifest
> + .signature(config)
> + .with_context(|| prefix.clone())?;
> + if target_fp == *source_fp.bytes() {
> + return Ok((None, true));
> + }
> +
> + bail!("Change detection fingerprint mismatch, refuse to continue!");
> + }
> + return Ok((None, false));
isn't this None here wrong? we know the source is encrypted, and the
target is not, so we should either abort, or return the key we found.
I sent some follow-ups, feel free to fold them in with either of these
two variants (they currently abort, which seems like the safe choice to
me).
> + }
> +
> + log_sender
> + .log(
> + Level::INFO,
> + format!("Found matching key '{key_id}' with fingerprint {key_fp}, decrypt on pull"),
> + )
> + .await?;
> +
> + Ok((Some(Arc::clone(config)), false))
> +}
> +
> /// Pulls a `snapshot`, removing newly created ones on error, but keeping existing ones in any case.
> ///
> /// The `reader` is configured to read from the source backup directory, while the
> --
> 2.47.3
>
>
>
>
>
>
next prev parent reply other threads:[~2026-04-24 12:46 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-04-24 10:36 [PATCH proxmox-backup 0/3] fixup for server side decryption Christian Ebner
2026-04-24 10:36 ` [PATCH proxmox-backup 1/3] sync: pull decrypt: check if contents only signed or fully encrypted Christian Ebner
2026-04-24 10:36 ` [PATCH proxmox-backup 2/3] sync: pull: refactor decryption key loading checks Christian Ebner
2026-04-24 12:46 ` Fabian Grünbichler [this message]
2026-04-24 10:36 ` [PATCH proxmox-backup 3/3] api: encryption keys: refactor associated keys check Christian Ebner
2026-04-24 12:46 ` [PATCH proxmox-backup 1/2] pull: decrypt: use let else to reduce indentation Fabian Grünbichler
2026-04-24 12:46 ` [PATCH proxmox-backup 2/2] pull: decrypt: re-order encryption key/state checks Fabian Grünbichler
2026-04-24 19:06 ` applied: [PATCH proxmox-backup 0/3] fixup for server side decryption Thomas Lamprecht
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=1777033255.umlrtuml3i.astroid@yuna.none \
--to=f.gruenbichler@proxmox.com \
--cc=c.ebner@proxmox.com \
--cc=pbs-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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox