use std::path::Path;
use base64::engine::general_purpose::STANDARD;
use base64::Engine as _;
#[must_use]
pub fn is_passphrase_protected(path: &Path) -> bool {
let Ok(content) = std::fs::read_to_string(path) else {
return false;
};
if content.contains("BEGIN ENCRYPTED PRIVATE KEY") || content.contains("Proc-Type: 4,ENCRYPTED")
{
return true;
}
if content.contains("BEGIN OPENSSH PRIVATE KEY") {
return is_openssh_key_passphrase_protected(&content);
}
false
}
fn is_openssh_key_passphrase_protected(pem: &str) -> bool {
let body: String = pem.lines().filter(|l| !l.starts_with("-----")).collect();
let Ok(decoded) = STANDARD.decode(body.as_bytes()) else {
return false;
};
let scan_len = decoded.len().min(100);
decoded[..scan_len].windows(6).any(|w| w == b"bcrypt")
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
fn project_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
}
#[test]
fn it_should_return_false_when_key_is_unencrypted() {
let key_path = project_root().join("fixtures/testing_rsa");
let result = is_passphrase_protected(&key_path);
assert!(
!result,
"Unencrypted key should not be detected as passphrase-protected"
);
}
#[test]
fn it_should_return_true_when_key_is_passphrase_protected() {
let key_path = project_root().join("fixtures/testing_ed25519_encrypted");
let result = is_passphrase_protected(&key_path);
assert!(result, "Passphrase-protected key should be detected");
}
#[test]
fn it_should_return_false_when_key_file_does_not_exist() {
let key_path = PathBuf::from("/nonexistent/path/to/key");
let result = is_passphrase_protected(&key_path);
assert!(
!result,
"Missing file should return false (no spurious warning)"
);
}
#[test]
fn it_should_return_true_when_legacy_pem_header_contains_encrypted() {
let dir = tempfile::TempDir::new().unwrap();
let key_path = dir.path().join("key.pem");
std::fs::write(
&key_path,
"-----BEGIN ENCRYPTED PRIVATE KEY-----\nZmFrZWtleQ==\n-----END ENCRYPTED PRIVATE KEY-----\n",
)
.unwrap();
let result = is_passphrase_protected(&key_path);
assert!(result);
}
}