use std::path::Path;
use toride_ssh_core::{CliRunner, Error, Result};
#[expect(
clippy::too_many_lines,
reason = "two code paths (in-process + ssh-keygen fallback)"
)]
pub async fn repair_public_key(
private_key_path: &Path,
passphrase: Option<&str>,
runner: &dyn CliRunner,
) -> Result<()> {
if !private_key_path.exists() {
return Err(Error::KeyNotFound(private_key_path.display().to_string()));
}
if !private_key_path.is_file() {
return Err(Error::KeyParseFailed(format!(
"{} is not a regular file",
private_key_path.display()
)));
}
let private_path = private_key_path.to_path_buf();
let in_process_result = tokio::task::spawn_blocking(move || {
let private_key_data = std::fs::read_to_string(&private_path).map_err(|e| {
Error::KeyParseFailed(format!(
"failed to read private key {}: {e}",
private_path.display()
))
})?;
let private_key = ssh_key::PrivateKey::from_openssh(&private_key_data).map_err(|e| {
Error::KeyParseFailed(format!(
"failed to parse private key {}: {e}",
private_path.display()
))
})?;
let public_path = private_path.with_extension("pub");
if public_path.exists() {
let backup_path = public_path.with_extension("pub.bak");
if let Err(e) = std::fs::rename(&public_path, &backup_path) {
tracing::warn!(
"failed to back up public key to {}: {e}",
backup_path.display()
);
}
}
let public_key = private_key.public_key();
if let Err(e) = public_key.write_openssh_file(&public_path) {
return Err(Error::KeyParseFailed(format!(
"failed to write public key {}: {e}",
public_path.display()
)));
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&public_path, std::fs::Permissions::from_mode(0o644))
.map_err(Error::Io)?;
}
Ok(())
})
.await
.map_err(|e| Error::TaskFailed(format!("repair_public_key task failed: {e}")))?;
if in_process_result.is_ok() {
return Ok(());
}
tracing::debug!(
"in-process key parse failed ({}), falling back to ssh-keygen",
in_process_result.as_ref().unwrap_err()
);
let path_str = private_key_path
.to_str()
.ok_or_else(|| Error::CommandFailed("key path is not valid UTF-8".to_owned()))?
.to_owned();
let args = vec!["-y".to_owned(), "-f".to_owned(), path_str];
let public_key_output = if let Some(pass) = passphrase
&& !pass.is_empty()
{
let askpass = toride_ssh_agent::AskpassHandler::new(pass)?;
crate::run_with_askpass(runner, "ssh-keygen", args, &askpass).await?
} else {
runner.run("ssh-keygen", args).await?
};
let public_path = private_key_path.with_extension("pub");
#[cfg(unix)]
let private_path = private_key_path.to_path_buf();
tokio::task::spawn_blocking(move || {
if public_path.exists() {
let backup_path = public_path.with_extension("pub.bak");
if let Err(e) = std::fs::rename(&public_path, &backup_path) {
tracing::warn!(
"failed to back up public key to {}: {e}",
backup_path.display()
);
}
}
let content = if public_key_output.ends_with('\n') {
public_key_output
} else {
format!("{public_key_output}\n")
};
std::fs::write(&public_path, &content).map_err(|e| {
Error::KeyParseFailed(format!(
"failed to write public key {}: {e}",
public_path.display()
))
})?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&public_path, std::fs::Permissions::from_mode(0o644))
.map_err(Error::Io)?;
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if private_path.exists()
&& let Err(e) =
std::fs::set_permissions(&private_path, std::fs::Permissions::from_mode(0o600))
{
tracing::warn!("failed to restore private key permissions: {e}");
}
}
Ok(())
})
.await
.map_err(|e| Error::TaskFailed(format!("repair_public_key write task failed: {e}")))?
}