use thiserror::Error;
pub use auths_crypto::AuthsErrorInfo;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AgentError {
#[error("Key not found")]
KeyNotFound,
#[error("Incorrect passphrase")]
IncorrectPassphrase,
#[error("Missing Passphrase")]
MissingPassphrase,
#[error("Security error: {0}")]
SecurityError(String),
#[error("Crypto error: {0}")]
CryptoError(String),
#[error("Key deserialization error: {0}")]
KeyDeserializationError(String),
#[error("Signing failed: {0}")]
SigningFailed(String),
#[error("Protocol error: {0}")]
Proto(String),
#[error("IO error: {0}")]
IO(#[from] std::io::Error),
#[error("git error: {0}")]
GitError(String),
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Mutex lock poisoned: {0}")]
MutexError(String),
#[error("Storage error: {0}")]
StorageError(String),
#[error("User input cancelled")]
UserInputCancelled,
#[error("Keychain backend unavailable: {backend} - {reason}")]
BackendUnavailable {
backend: &'static str,
reason: String,
},
#[error("Storage is locked, authentication required")]
StorageLocked,
#[error("Failed to initialize keychain backend: {backend} - {error}")]
BackendInitFailed {
backend: &'static str,
error: String,
},
#[error("Credential too large for backend (max {max_bytes} bytes, got {actual_bytes})")]
CredentialTooLarge {
max_bytes: usize,
actual_bytes: usize,
},
#[error("Agent is locked. Unlock with 'auths agent unlock' or restart the agent.")]
AgentLocked,
#[error("Passphrase too weak: {0}")]
WeakPassphrase(String),
#[error("HSM PIN is locked — reset required")]
HsmPinLocked,
#[error("HSM device removed")]
HsmDeviceRemoved,
#[error("HSM session expired")]
HsmSessionExpired,
#[error("HSM does not support mechanism: {0}")]
HsmUnsupportedMechanism(String),
#[error(
"Operation '{operation}' requires a software-backed key; hardware-backed keys (e.g. Secure Enclave) cannot export raw material"
)]
HardwareKeyNotExportable {
operation: String,
},
}
impl AuthsErrorInfo for AgentError {
fn error_code(&self) -> &'static str {
match self {
Self::KeyNotFound => "AUTHS-E3001",
Self::IncorrectPassphrase => "AUTHS-E3002",
Self::MissingPassphrase => "AUTHS-E3003",
Self::SecurityError(_) => "AUTHS-E3004",
Self::CryptoError(_) => "AUTHS-E3005",
Self::KeyDeserializationError(_) => "AUTHS-E3006",
Self::SigningFailed(_) => "AUTHS-E3007",
Self::Proto(_) => "AUTHS-E3008",
Self::IO(_) => "AUTHS-E3009",
Self::GitError(_) => "AUTHS-E3010",
Self::InvalidInput(_) => "AUTHS-E3011",
Self::MutexError(_) => "AUTHS-E3012",
Self::StorageError(_) => "AUTHS-E3013",
Self::UserInputCancelled => "AUTHS-E3014",
Self::BackendUnavailable { .. } => "AUTHS-E3015",
Self::StorageLocked => "AUTHS-E3016",
Self::BackendInitFailed { .. } => "AUTHS-E3017",
Self::CredentialTooLarge { .. } => "AUTHS-E3018",
Self::AgentLocked => "AUTHS-E3019",
Self::WeakPassphrase(_) => "AUTHS-E3020",
Self::HsmPinLocked => "AUTHS-E3021",
Self::HsmDeviceRemoved => "AUTHS-E3022",
Self::HsmSessionExpired => "AUTHS-E3023",
Self::HsmUnsupportedMechanism(_) => "AUTHS-E3024",
Self::HardwareKeyNotExportable { .. } => "AUTHS-E3025",
}
}
fn suggestion(&self) -> Option<&'static str> {
match self {
Self::KeyNotFound => Some("Run `auths key list` to see available keys"),
Self::IncorrectPassphrase => Some(
"Check your passphrase and try again. Set AUTHS_PASSPHRASE for automation, or run `auths agent start` for session caching",
),
Self::MissingPassphrase => {
Some("Provide a passphrase with --passphrase or set AUTHS_PASSPHRASE")
}
Self::BackendUnavailable { .. } => {
Some("Run `auths doctor` to diagnose keychain issues")
}
Self::StorageLocked => Some("Authenticate with your platform keychain"),
Self::BackendInitFailed { .. } => {
Some("Run `auths doctor` to diagnose keychain issues")
}
Self::GitError(_) => Some("Ensure you're in a Git repository"),
Self::AgentLocked => {
Some("Run `auths agent unlock` or restart with `auths agent start`")
}
Self::UserInputCancelled => {
Some("Run the command again and provide the required input")
}
Self::StorageError(_) => Some("Check file permissions and disk space"),
Self::SecurityError(_) => Some(
"Run `auths doctor` to check system keychain access and security configuration",
),
Self::CryptoError(_) => {
Some("A cryptographic operation failed; check key material with `auths key list`")
}
Self::KeyDeserializationError(_) => {
Some("The stored key is corrupted; re-import with `auths key import`")
}
Self::SigningFailed(_) => Some(
"The signing operation failed; verify your key is accessible with `auths key list`",
),
Self::Proto(_) => Some(
"A protocol error occurred; check that both sides are running compatible versions",
),
Self::IO(_) => Some("Check file permissions and that the filesystem is not read-only"),
Self::InvalidInput(_) => Some("Check the command arguments and try again"),
Self::MutexError(_) => Some("A concurrency error occurred; restart the operation"),
Self::CredentialTooLarge { .. } => Some(
"Reduce the credential size or use file-based storage with AUTHS_KEYCHAIN_BACKEND=file",
),
Self::WeakPassphrase(_) => {
Some("Use at least 12 characters with uppercase, lowercase, and a digit or symbol")
}
Self::HsmPinLocked => Some("Reset the HSM PIN using your HSM vendor's admin tools"),
Self::HsmDeviceRemoved => Some("Reconnect the HSM device and try again"),
Self::HsmSessionExpired => Some("Retry the operation — a new session will be opened"),
Self::HsmUnsupportedMechanism(_) => {
Some("Check that your HSM supports Ed25519 (CKM_EDDSA)")
}
Self::HardwareKeyNotExportable { .. } => Some(
"Use a software-backed keychain backend for this operation, or re-initialize your identity without Secure Enclave",
),
}
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TrustError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("{0}")]
InvalidData(String),
#[error("not found: {0}")]
NotFound(String),
#[error("serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("already exists: {0}")]
AlreadyExists(String),
#[error("lock acquisition failed: {0}")]
Lock(String),
#[error("policy rejected: {0}")]
PolicyRejected(String),
}
impl AuthsErrorInfo for TrustError {
fn error_code(&self) -> &'static str {
match self {
Self::Io(_) => "AUTHS-E3101",
Self::InvalidData(_) => "AUTHS-E3102",
Self::NotFound(_) => "AUTHS-E3103",
Self::Serialization(_) => "AUTHS-E3104",
Self::AlreadyExists(_) => "AUTHS-E3105",
Self::Lock(_) => "AUTHS-E3106",
Self::PolicyRejected(_) => "AUTHS-E3107",
}
}
fn suggestion(&self) -> Option<&'static str> {
match self {
Self::NotFound(_) => Some("Run `auths trust list` to see pinned identities"),
Self::PolicyRejected(_) => Some("Run `auths trust pin` to pin this identity"),
Self::Lock(_) => Some("Check file permissions and try again"),
Self::Io(_) => Some("Check disk space and file permissions"),
Self::AlreadyExists(_) => Some("Run `auths trust list` to see existing entries"),
Self::InvalidData(_) => {
Some("The trust store may be corrupted; delete and re-pin with `auths trust pin`")
}
Self::Serialization(_) => {
Some("The trust store data is corrupted; delete and re-pin with `auths trust pin`")
}
}
}
}
impl From<AgentError> for ssh_agent_lib::error::AgentError {
fn from(err: AgentError) -> Self {
match err {
AgentError::KeyNotFound => Self::Failure,
AgentError::IncorrectPassphrase => Self::Failure,
_ => Self::Failure,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hardware_key_not_exportable_has_actionable_display() {
let err = AgentError::HardwareKeyNotExportable {
operation: "pairing".into(),
};
let msg = err.to_string();
assert!(msg.contains("hardware"), "msg={msg}");
assert!(msg.contains("pairing"), "msg={msg}");
}
}