@@ -66,6 +66,10 @@ pub enum AgeVaultError {
6666 /// The key file could not be written to disk.
6767 #[ error( "failed to write key file: {0}" ) ]
6868 KeyWrite ( std:: io:: Error ) ,
69+ /// [`AgeVaultProvider::set_secret_mut`] was called with `overwrite: false` for a key that
70+ /// already exists in the vault.
71+ #[ error( "secret key already exists: {0} (pass overwrite=true to replace it)" ) ]
72+ AlreadyExists ( String ) ,
6973}
7074
7175// ---------------------------------------------------------------------------
@@ -249,7 +253,7 @@ impl AgeVaultProvider {
249253 /// Path::new("/etc/zeph/vault-key.txt"),
250254 /// Path::new("/etc/zeph/secrets.age"),
251255 /// )?;
252- /// vault.set_secret_mut("MY_TOKEN".into(), "tok_abc123".into()) ;
256+ /// vault.set_secret_mut("MY_TOKEN".into(), "tok_abc123".into(), false)? ;
253257 /// vault.save()?;
254258 /// # Ok::<_, zeph_vault::AgeVaultError>(())
255259 /// ```
@@ -282,7 +286,7 @@ impl AgeVaultProvider {
282286 /// Path::new("/etc/zeph/vault-key.txt"),
283287 /// Path::new("/etc/zeph/secrets.age"),
284288 /// )?;
285- /// vault.set_secret_mut("MY_TOKEN".into(), "tok_abc123".into()) ;
289+ /// vault.set_secret_mut("MY_TOKEN".into(), "tok_abc123".into(), false)? ;
286290 /// vault.save_async().await?;
287291 /// # Ok(())
288292 /// # }
@@ -309,8 +313,19 @@ impl AgeVaultProvider {
309313
310314 /// Insert or update a secret in the in-memory map.
311315 ///
316+ /// Refuses to replace an existing key unless `overwrite` is `true`, so that callers cannot
317+ /// silently destroy a previously-stored secret by accident — see #5955 (and the sibling
318+ /// incident #5874, which hit the same gap in the `zeph init` durable-execution wizard before
319+ /// this guard existed at the vault layer). Callers that intend an unconditional update (e.g.
320+ /// OAuth token refresh) pass `overwrite: true` explicitly.
321+ ///
312322 /// Call [`save`][Self::save] afterwards to persist the change to disk.
313323 ///
324+ /// # Errors
325+ ///
326+ /// Returns [`AgeVaultError::AlreadyExists`] if `key` is already present and `overwrite` is
327+ /// `false`. The in-memory map is left untouched in that case.
328+ ///
314329 /// # Examples
315330 ///
316331 /// ```no_run
@@ -321,12 +336,21 @@ impl AgeVaultProvider {
321336 /// Path::new("/etc/zeph/vault-key.txt"),
322337 /// Path::new("/etc/zeph/secrets.age"),
323338 /// )?;
324- /// vault.set_secret_mut("API_KEY".into(), "sk-...".into()) ;
339+ /// vault.set_secret_mut("API_KEY".into(), "sk-...".into(), false)? ;
325340 /// vault.save()?;
326341 /// # Ok::<_, zeph_vault::AgeVaultError>(())
327342 /// ```
328- pub fn set_secret_mut ( & mut self , key : String , value : String ) {
343+ pub fn set_secret_mut (
344+ & mut self ,
345+ key : String ,
346+ value : String ,
347+ overwrite : bool ,
348+ ) -> Result < ( ) , AgeVaultError > {
349+ if !overwrite && self . secrets . contains_key ( & key) {
350+ return Err ( AgeVaultError :: AlreadyExists ( key) ) ;
351+ }
329352 self . secrets . insert ( key, Zeroizing :: new ( value) ) ;
353+ Ok ( ( ) )
330354 }
331355
332356 /// Remove a secret from the in-memory map.
@@ -563,7 +587,9 @@ mod tests {
563587 let ( key_path, vault_path) = init_temp_vault ( dir. path ( ) ) ;
564588
565589 let mut vault = AgeVaultProvider :: new ( & key_path, & vault_path) . unwrap ( ) ;
566- vault. set_secret_mut ( "KEY" . into ( ) , "val" . into ( ) ) ;
590+ vault
591+ . set_secret_mut ( "KEY" . into ( ) , "val" . into ( ) , false )
592+ . unwrap ( ) ;
567593 vault. save ( ) . unwrap ( ) ;
568594
569595 let loaded = AgeVaultProvider :: load ( & key_path, & vault_path) . unwrap ( ) ;
@@ -576,7 +602,9 @@ mod tests {
576602 let ( key_path, vault_path) = init_temp_vault ( dir. path ( ) ) ;
577603
578604 let mut vault = AgeVaultProvider :: new ( & key_path, & vault_path) . unwrap ( ) ;
579- vault. set_secret_mut ( "KEY" . into ( ) , "val" . into ( ) ) ;
605+ vault
606+ . set_secret_mut ( "KEY" . into ( ) , "val" . into ( ) , false )
607+ . unwrap ( ) ;
580608
581609 assert ! ( vault. remove_secret_mut( "KEY" ) ) ;
582610 assert ! ( !vault. remove_secret_mut( "KEY" ) ) ;
@@ -656,11 +684,50 @@ mod tests {
656684 let ( key_path, vault_path) = init_temp_vault ( dir. path ( ) ) ;
657685
658686 let mut vault = AgeVaultProvider :: new ( & key_path, & vault_path) . unwrap ( ) ;
659- vault. set_secret_mut ( "TMP_TEST" . into ( ) , "value" . into ( ) ) ;
687+ vault
688+ . set_secret_mut ( "TMP_TEST" . into ( ) , "value" . into ( ) , false )
689+ . unwrap ( ) ;
660690 vault. save ( ) . unwrap ( ) ;
661691
662692 let tmp_path = vault_path. with_added_extension ( "tmp" ) ;
663693 assert ! ( !tmp_path. exists( ) , ".age.tmp must not exist after save()" ) ;
664694 assert ! ( vault_path. exists( ) , "secrets.age must exist after save()" ) ;
665695 }
696+
697+ /// Regression for #5955: `set_secret_mut` must refuse to replace an existing key when
698+ /// `overwrite` is `false`, and must leave the previous value untouched.
699+ #[ test]
700+ fn set_secret_mut_rejects_overwrite_when_not_requested ( ) {
701+ let dir = tempdir ( ) . unwrap ( ) ;
702+ let ( key_path, vault_path) = init_temp_vault ( dir. path ( ) ) ;
703+
704+ let mut vault = AgeVaultProvider :: new ( & key_path, & vault_path) . unwrap ( ) ;
705+ vault
706+ . set_secret_mut ( "KEY" . into ( ) , "original" . into ( ) , false )
707+ . unwrap ( ) ;
708+
709+ let result = vault. set_secret_mut ( "KEY" . into ( ) , "clobbered" . into ( ) , false ) ;
710+ assert ! (
711+ matches!( result, Err ( AgeVaultError :: AlreadyExists ( ref k) ) if k == "KEY" ) ,
712+ "expected AlreadyExists(\" KEY\" ), got {result:?}" ,
713+ ) ;
714+ assert_eq ! ( vault. get( "KEY" ) , Some ( "original" ) ) ;
715+ }
716+
717+ /// Regression for #5955: `overwrite: true` must replace an existing value.
718+ #[ test]
719+ fn set_secret_mut_replaces_when_overwrite_requested ( ) {
720+ let dir = tempdir ( ) . unwrap ( ) ;
721+ let ( key_path, vault_path) = init_temp_vault ( dir. path ( ) ) ;
722+
723+ let mut vault = AgeVaultProvider :: new ( & key_path, & vault_path) . unwrap ( ) ;
724+ vault
725+ . set_secret_mut ( "KEY" . into ( ) , "original" . into ( ) , false )
726+ . unwrap ( ) ;
727+ vault
728+ . set_secret_mut ( "KEY" . into ( ) , "updated" . into ( ) , true )
729+ . unwrap ( ) ;
730+
731+ assert_eq ! ( vault. get( "KEY" ) , Some ( "updated" ) ) ;
732+ }
666733}
0 commit comments