//! Private-key loading for SSH authentication (OpenSSH, PKCS#1/#5/#8 PEM, PuTTY PPK) and //! the public-key fingerprint used for lockout protection in Users. use crate::core::error::{CoreError, CoreResult}; use base64::Engine; use russh::keys::PrivateKey; use sha2::{Digest, Sha256}; use std::path::Path; pub const MAX_KEY_BYTES: u64 = 2 * 1024 * 1024; pub fn read_private_key_file(path: &str) -> CoreResult { if path.to_lowercase().ends_with(".pub") { return Err(CoreError::new("PUBLIC_KEY_SELECTED", "Select a private key, not a .pub file.")); } let meta = std::fs::metadata(path).map_err(|_| CoreError::new("PRIVATE_KEY_NOT_FOUND", "The private key file cannot be read."))?; if !meta.is_file() || meta.len() == 0 || meta.len() > MAX_KEY_BYTES { return Err(CoreError::new("PRIVATE_KEY_INVALID", "The private key file is invalid or too large.")); } let bytes = std::fs::read(path).map_err(|_| CoreError::new("PRIVATE_KEY_NOT_FOUND", "The private key file cannot be read."))?; String::from_utf8(bytes).map_err(|_| CoreError::new("PRIVATE_KEY_INVALID", "The private key file is not a supported text key format.")) } pub fn decode_private_key(text: &str, passphrase: Option<&str>) -> CoreResult { let passphrase = passphrase.filter(|p| !p.is_empty()); match russh::keys::decode_secret_key(text, passphrase) { Ok(key) => Ok(key), Err(russh::keys::Error::KeyIsEncrypted) => Err(CoreError::new("PRIVATE_KEY_PASSPHRASE_REQUIRED", "The private key is encrypted; enter its passphrase.")), Err(error) => Err(CoreError::new( if passphrase.is_some() { "PRIVATE_KEY_PASSPHRASE_INVALID" } else { "PRIVATE_KEY_UNSUPPORTED" }, format!("Cannot parse privateKey: {error}"), )), } } pub fn fingerprint_of_blob(blob: &[u8]) -> String { format!("SHA256:{}", base64::engine::general_purpose::STANDARD_NO_PAD.encode(Sha256::digest(blob))) } /// `SHA256:…` fingerprint of the public half of a private key file (or its `.pub`). pub fn public_key_fingerprint_for_private_key(path: &str) -> Option { if let Ok(text) = std::fs::read_to_string(path) { // Public half of an OpenSSH container is readable without the passphrase. if let Ok(key) = ssh_key_public_from_openssh(&text) { return Some(fingerprint_of_blob(&key)); } if let Ok(key) = russh::keys::decode_secret_key(&text, None) { if let Ok(blob) = key.public_key().to_bytes() { return Some(fingerprint_of_blob(&blob)); } } } let public = std::fs::read_to_string(format!("{path}.pub")).ok()?; let blob = base64::engine::general_purpose::STANDARD.decode(public.split_whitespace().nth(1)?).ok()?; (!blob.is_empty()).then(|| fingerprint_of_blob(&blob)) } fn ssh_key_public_from_openssh(text: &str) -> CoreResult> { let key = russh::keys::ssh_key::PrivateKey::from_openssh(text.trim()).map_err(|e| CoreError::plain(e.to_string()))?; key.public_key().to_bytes().map_err(|e| CoreError::plain(e.to_string())) } pub fn sha256_hex(blob: &[u8]) -> String { hex::encode(Sha256::digest(blob)) } pub fn exists(path: &str) -> bool { Path::new(path).is_file() }