fix: A critical security flaw was discovered that essentially had all local secrets be encrypted with an all-zero key

This commit is contained in:
2026-02-01 16:15:13 -07:00
parent c95bae1761
commit 9944e29ef0
8 changed files with 412 additions and 88 deletions
+12 -18
View File
@@ -46,20 +46,26 @@ providers:
password_file.display()
)
};
// Confy with yaml feature typically uses .yml; write both to be safe.
fs::write(app_dir.join("config.yml"), &cfg).unwrap();
fs::write(app_dir.join("config.yaml"), &cfg).unwrap();
}
fn create_password_file(path: &Path, content: &[u8]) {
fs::write(path, content).unwrap();
#[cfg(unix)]
{
fs::set_permissions(path, fs::Permissions::from_mode(0o600)).unwrap();
}
}
#[test]
#[cfg(unix)]
fn cli_config_no_changes() {
let (td, xdg_cfg, xdg_cache) = setup_env();
let pw_file = td.path().join("pw.txt");
fs::write(&pw_file, b"pw\n").unwrap();
create_password_file(&pw_file, b"pw\n");
write_yaml_config(&xdg_cfg, &pw_file, None);
// Create a no-op editor script that exits successfully without modifying the file
let editor = td.path().join("noop-editor.sh");
fs::write(&editor, b"#!/bin/sh\nexit 0\n").unwrap();
let mut perms = fs::metadata(&editor).unwrap().permissions();
@@ -82,10 +88,9 @@ fn cli_config_no_changes() {
fn cli_config_updates_and_persists() {
let (td, xdg_cfg, xdg_cache) = setup_env();
let pw_file = td.path().join("pw.txt");
fs::write(&pw_file, b"pw\n").unwrap();
create_password_file(&pw_file, b"pw\n");
write_yaml_config(&xdg_cfg, &pw_file, None);
// Editor script appends a valid run_configs section to the YAML file
let editor = td.path().join("append-run-config.sh");
let script = r#"#!/bin/sh
FILE="$1"
@@ -111,7 +116,6 @@ exit 0
"Configuration updated successfully",
));
// Verify that the config file now contains the run_configs key
let cfg_path = xdg_cfg.join("gman").join("config.yml");
let written = fs::read_to_string(&cfg_path).expect("config file readable");
assert!(written.contains("run_configs:"));
@@ -134,10 +138,9 @@ fn cli_shows_help() {
fn cli_add_get_list_update_delete_roundtrip() {
let (td, xdg_cfg, xdg_cache) = setup_env();
let pw_file = td.path().join("pw.txt");
fs::write(&pw_file, b"testpw\n").unwrap();
create_password_file(&pw_file, b"testpw\n");
write_yaml_config(&xdg_cfg, &pw_file, None);
// add
let mut add = Command::cargo_bin("gman").unwrap();
add.env("XDG_CONFIG_HOME", &xdg_cfg)
.env("XDG_CACHE_HOME", &xdg_cache)
@@ -154,7 +157,6 @@ fn cli_add_get_list_update_delete_roundtrip() {
let add_out = child.wait_with_output().unwrap();
assert!(add_out.status.success());
// get (text)
let mut get = Command::cargo_bin("gman").unwrap();
get.env("XDG_CONFIG_HOME", &xdg_cfg)
.env("XDG_CACHE_HOME", &xdg_cache)
@@ -163,7 +165,6 @@ fn cli_add_get_list_update_delete_roundtrip() {
.success()
.stdout(predicate::str::contains("super_secret"));
// get as JSON
let mut get_json = Command::cargo_bin("gman").unwrap();
get_json
.env("XDG_CONFIG_HOME", &xdg_cfg)
@@ -173,7 +174,6 @@ fn cli_add_get_list_update_delete_roundtrip() {
predicate::str::contains("my_api_key").and(predicate::str::contains("super_secret")),
);
// list
let mut list = Command::cargo_bin("gman").unwrap();
list.env("XDG_CONFIG_HOME", &xdg_cfg)
.env("XDG_CACHE_HOME", &xdg_cache)
@@ -182,7 +182,6 @@ fn cli_add_get_list_update_delete_roundtrip() {
.success()
.stdout(predicate::str::contains("my_api_key"));
// update
let mut update = Command::cargo_bin("gman").unwrap();
update
.env("XDG_CONFIG_HOME", &xdg_cfg)
@@ -199,7 +198,6 @@ fn cli_add_get_list_update_delete_roundtrip() {
let upd_out = child.wait_with_output().unwrap();
assert!(upd_out.status.success());
// get again
let mut get2 = Command::cargo_bin("gman").unwrap();
get2.env("XDG_CONFIG_HOME", &xdg_cfg)
.env("XDG_CACHE_HOME", &xdg_cache)
@@ -208,14 +206,12 @@ fn cli_add_get_list_update_delete_roundtrip() {
.success()
.stdout(predicate::str::contains("new_val"));
// delete
let mut del = Command::cargo_bin("gman").unwrap();
del.env("XDG_CONFIG_HOME", &xdg_cfg)
.env("XDG_CACHE_HOME", &xdg_cache)
.args(["delete", "my_api_key"]);
del.assert().success();
// get should now fail
let mut get_missing = Command::cargo_bin("gman").unwrap();
get_missing
.env("XDG_CONFIG_HOME", &xdg_cfg)
@@ -228,10 +224,9 @@ fn cli_add_get_list_update_delete_roundtrip() {
fn cli_wrap_dry_run_env_injection() {
let (td, xdg_cfg, xdg_cache) = setup_env();
let pw_file = td.path().join("pw.txt");
fs::write(&pw_file, b"pw\n").unwrap();
create_password_file(&pw_file, b"pw\n");
write_yaml_config(&xdg_cfg, &pw_file, Some("echo"));
// Add the secret so the profile can read it
let mut add = Command::cargo_bin("gman").unwrap();
add.env("XDG_CONFIG_HOME", &xdg_cfg)
.env("XDG_CACHE_HOME", &xdg_cache)
@@ -243,7 +238,6 @@ fn cli_wrap_dry_run_env_injection() {
let add_out = child.wait_with_output().unwrap();
assert!(add_out.status.success());
// Dry-run wrapping: prints preview command
let mut wrap = Command::cargo_bin("gman").unwrap();
wrap.env("XDG_CONFIG_HOME", &xdg_cfg)
.env("XDG_CACHE_HOME", &xdg_cache)
+8
View File
@@ -0,0 +1,8 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 155469a45d7311cd4003e23a3bcdaa8e55879e6222c1b6313a2b1f0b563bb195 # shrinks to password = "", msg = " "
cc 0bc9f608677234c082d10ff51b15dc39b4c194cdf920b4d87e553467c93824ed # shrinks to password = "", msg = ""
+2 -3
View File
@@ -9,7 +9,7 @@ use secrecy::SecretString;
proptest! {
#[test]
fn prop_encrypt_decrypt_roundtrip(password in ".{0,64}", msg in ".{0,512}") {
fn prop_encrypt_decrypt_roundtrip(password in ".{1,64}", msg in ".{0,512}") {
let pw = SecretString::new(password.into());
let env = encrypt_string(pw.clone(), &msg).unwrap();
let out = decrypt_string(pw, &env).unwrap();
@@ -18,10 +18,9 @@ proptest! {
}
#[test]
fn prop_tamper_ciphertext_detected(password in ".{0,32}", msg in ".{1,128}") {
fn prop_tamper_ciphertext_detected(password in ".{1,32}", msg in ".{1,128}") {
let pw = SecretString::new(password.into());
let env = encrypt_string(pw.clone(), &msg).unwrap();
// Flip a bit in the ct payload segment
let mut parts: Vec<&str> = env.split(';').collect();
let ct_b64 = parts[6].strip_prefix("ct=").unwrap();
let mut ct = base64::engine::general_purpose::STANDARD.decode(ct_b64).unwrap();