#![cfg(unix)] #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod common;
use std::ffi::OsString;
use std::fs;
use std::os::unix::ffi::OsStringExt;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
use decdn_incentive::eth_identity;
use tempfile::TempDir;
fn dirs() -> (TempDir, TempDir) {
let home = TempDir::new().unwrap();
let out = TempDir::new().unwrap();
fs::set_permissions(out.path(), fs::Permissions::from_mode(0o700)).unwrap();
(home, out)
}
fn key_gen(home: &Path, out: &Path) -> Command {
let mut cmd = common::decdn_command(home);
cmd.args(["key-gen", "--output-dir"])
.arg(out)
.stdin(Stdio::null());
cmd
}
fn run(cmd: &mut Command) -> Output {
cmd.output().expect("spawn decdn")
}
fn keystore(out: &TempDir) -> PathBuf {
eth_identity::keystore_path(out.path())
}
#[test]
fn creating_under_an_empty_env_password_is_refused_naming_the_source() {
let (home, out) = dirs();
let pw_file = out.path().join("pw.txt");
fs::write(&pw_file, b"not-this-one").unwrap();
let output = run(key_gen(home.path(), out.path())
.arg("--keystore-password-file")
.arg(&pw_file)
.env(eth_identity::KEYSTORE_PASSWORD_ENV, ""));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"an empty env password must not create a keystore: {stderr}"
);
assert!(
stderr.contains(eth_identity::KEYSTORE_PASSWORD_ENV),
"the error must name the env source: {stderr}"
);
assert!(
!keystore(&out).exists(),
"no key material may be written when an empty env password is refused"
);
assert!(
!decdn_common::identity::key_path(out.path()).exists(),
"node.secret must not be half-written either"
);
}
#[test]
fn a_shadowed_password_file_warns_and_env_still_wins() {
let (home, out) = dirs();
let pw_file = out.path().join("pw.txt");
fs::write(&pw_file, b"file-password").unwrap();
let output = run(key_gen(home.path(), out.path())
.arg("--keystore-password-file")
.arg(&pw_file)
.env(eth_identity::KEYSTORE_PASSWORD_ENV, "env-password"));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "key-gen failed: {stderr}");
eth_identity::load_signer(&keystore(&out), "env-password")
.expect("the env var must win over the shadowed file");
assert!(
stderr.contains(&pw_file.display().to_string()) && stderr.contains("was not used"),
"a shadowed password file must be named in a warning: {stderr}"
);
}
#[test]
fn an_unset_env_password_falls_through_to_the_password_file() {
let (home, out) = dirs();
let pw_file = out.path().join("pw.txt");
fs::write(&pw_file, b"hunter2\n").unwrap();
let output = run(key_gen(home.path(), out.path())
.arg("--keystore-password-file")
.arg(&pw_file));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "key-gen failed: {stderr}");
eth_identity::load_signer(&keystore(&out), "hunter2").expect("the file password must open it");
assert!(
!stderr.contains("EMPTY password"),
"a non-empty password must not warn: {stderr}"
);
}
#[test]
fn an_empty_password_file_creates_an_empty_password_keystore() {
let (home, out) = dirs();
let pw_file = out.path().join("empty.pw");
fs::write(&pw_file, b"").unwrap();
let output = run(key_gen(home.path(), out.path())
.arg("--keystore-password-file")
.arg(&pw_file));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "key-gen failed: {stderr}");
eth_identity::load_signer(&keystore(&out), "").expect("the empty password must open it");
assert!(stderr.contains("EMPTY password"), "must warn: {stderr}");
}
#[test]
fn a_non_utf8_env_password_is_fatal_and_does_not_echo_the_value() {
let (home, out) = dirs();
let pw_file = out.path().join("pw.txt");
fs::write(&pw_file, b"not-this-one").unwrap();
let bad = OsString::from_vec(vec![0x70, 0xff, 0x77]);
let output = run(key_gen(home.path(), out.path())
.arg("--keystore-password-file")
.arg(&pw_file)
.env(eth_identity::KEYSTORE_PASSWORD_ENV, &bad));
assert!(
!output.status.success(),
"a set-but-unreadable env var must not fall through to the file"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("is not valid UTF-8"),
"the error must name the cause: {stderr}"
);
assert!(
!stderr.contains("\u{fffd}") && !stderr.contains("pw"),
"the error must not echo the value: {stderr}"
);
assert!(
!keystore(&out).exists(),
"no key material may be written on a fatal password error"
);
}
#[test]
fn a_tilde_password_file_is_expanded_against_home() {
let (home, out) = dirs();
fs::write(home.path().join("pw.txt"), b"hunter2\n").unwrap();
let output = run(key_gen(home.path(), out.path())
.arg("--keystore-password-file")
.arg("~/pw.txt"));
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "key-gen failed: {stderr}");
eth_identity::load_signer(&keystore(&out), "hunter2")
.expect("the tilde path must resolve to the file under HOME");
}
#[test]
fn a_missing_password_file_fails_naming_the_path() {
let (home, out) = dirs();
let missing = out.path().join("nope.pw");
let output = run(key_gen(home.path(), out.path())
.arg("--keystore-password-file")
.arg(&missing));
assert!(!output.status.success(), "a missing file must not succeed");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("no keystore password source"),
"got: {stderr}"
);
assert!(
stderr.contains(&missing.display().to_string()),
"the error must name the path tried: {stderr}"
);
assert!(
!keystore(&out).exists(),
"no key material may be written when the password cannot be sourced"
);
assert!(
!decdn_common::identity::key_path(out.path()).exists(),
"node.secret must not be half-written either — the password is sourced \
before any disk write"
);
}