use std::error::Error; use std::convert::AsRef; use std::ffi::OsString; use std::string::FromUtf8Error; use strum_macros::AsRefStr; use thiserror::Error as ThisError; use aws_sdk_sts::{ error::SdkError as AwsSdkError, operation::get_session_token::GetSessionTokenError, error::ProvideErrorMetadata, }; use rfd::{ AsyncMessageDialog, MessageLevel, }; use sqlx::{ error::Error as SqlxError, migrate::MigrateError, }; use tauri::async_runtime as rt; use tauri_plugin_global_shortcut::Error as ShortcutError; use tokio::sync::oneshot::error::RecvError; use serde::{ Serialize, Serializer, ser::SerializeMap, Deserialize, }; pub trait ShowError { fn error_popup(self, title: &str); fn error_print(self); fn error_print_prefix(self, prefix: &str); } impl ShowError for Result where E: std::fmt::Display { fn error_popup(self, title: &str) { if let Err(e) = self { let dialog = AsyncMessageDialog::new() .set_level(MessageLevel::Error) .set_title(title) .set_description(format!("{e}")); rt::spawn(async move {dialog.show().await}); } } fn error_print(self) { if let Err(e) = self { eprintln!("{e}"); } } fn error_print_prefix(self, prefix: &str) { if let Err(e) = self { eprintln!("{prefix}: {e}"); } } } fn serialize_basic_err(err: &E, serializer: S) -> Result where E: std::error::Error + AsRef, S: Serializer, { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", err.as_ref())?; map.serialize_entry("msg", &format!("{err}"))?; if let Some(src) = err.source() { map.serialize_entry("source", &format!("{src}"))?; } map.end() } struct SerializeUpstream(pub E); impl Serialize for SerializeUpstream { fn serialize(&self, serializer: S) -> Result { let msg = format!("{}", self.0); let mut map = serializer.serialize_map(None)?; map.serialize_entry("msg", &msg)?; map.serialize_entry("code", &None::<&str>)?; map.serialize_entry("source", &None::<&str>)?; map.end() } } fn serialize_upstream_err(err: &E, map: &mut M) -> Result<(), M::Error> where E: Error, M: serde::ser::SerializeMap, { // let msg = err.source().map(|s| format!("{s}")); // map.serialize_entry("msg", &msg)?; // map.serialize_entry("code", &None::<&str>)?; // map.serialize_entry("source", &None::<&str>)?; match err.source() { Some(src) => map.serialize_entry("source", &SerializeUpstream(src))?, None => map.serialize_entry("source", &None::<&str>)?, } Ok(()) } macro_rules! impl_serialize_basic { ($err_type:ident) => { impl Serialize for $err_type { fn serialize(&self, serializer: S) -> Result { serialize_basic_err(self, serializer) } } } } // error during initial setup (primarily loading state from db) #[derive(Debug, ThisError, AsRefStr)] pub enum SetupError { #[error("Invalid database record")] InvalidRecord, // e.g. wrong size blob for nonce or salt #[error("Error from database: {0}")] DbError(#[from] SqlxError), #[error("Error loading data: {0}")] KvError(#[from] LoadKvError), #[error("Error running migrations: {0}")] MigrationError(#[from] MigrateError), #[error("Failed to set up start-on-login: {0}")] AutoLaunchError(#[from] auto_launch::Error), #[error("Failed to start listener: {0}")] ServerSetupError(#[from] std::io::Error), #[error("Failed to resolve data directory: {0}")] DataDir(#[from] DataDirError), #[error("Failed to register hotkeys: {0}")] RegisterHotkeys(#[from] ShortcutError), } #[derive(Debug, ThisError, AsRefStr)] pub enum DataDirError { #[error("Could not determine data directory")] NotFound, #[error("Failed to create data directory: {0}")] Io(#[from] std::io::Error), } // error when attempting to tell a request handler whether to release or deny credentials #[derive(Debug, ThisError, AsRefStr)] pub enum SendResponseError { #[error("The specified credentials request was not found")] NotFound, #[error("The specified request was already closed by the client")] Abandoned, #[error("A response has already been received for the specified request")] Fulfilled, #[error("Could not renew AWS sesssion: {0}")] SessionRenew(#[from] GetSessionError), } // errors encountered while handling a client request #[derive(Debug, ThisError, AsRefStr)] pub enum HandlerError { #[error("Error writing to stream: {0}")] StreamIOError(#[from] std::io::Error), #[error("Received invalid UTF-8 in request")] InvalidUtf8(#[from] FromUtf8Error), #[error("HTTP request malformed")] BadRequest(#[from] serde_json::Error), #[error("HTTP request too large")] RequestTooLarge, #[error("Connection closed early by client")] Abandoned, #[error("Internal server error")] Internal(#[from] RecvError), #[error("Error accessing credentials: {0}")] NoCredentials(#[from] GetCredentialsError), #[error("Error getting client details: {0}")] ClientInfo(#[from] ClientInfoError), #[error("Error from Tauri: {0}")] Tauri(#[from] tauri::Error), #[error("No main application window found")] NoMainWindow, #[error("Request was denied")] Denied, } #[derive(Debug, ThisError, AsRefStr)] pub enum WindowError { #[error("Failed to find main application window")] NoMainWindow, #[error(transparent)] ManageFailure(#[from] tauri::Error), } #[derive(Debug, ThisError, AsRefStr)] pub enum GetCredentialsError { #[error("Credentials are currently locked")] Locked, #[error("No credentials are known")] Empty, #[error(transparent)] Crypto(#[from] CryptoError), #[error(transparent)] Load(#[from] LoadCredentialsError), #[error(transparent)] GetSession(#[from] GetSessionError), } #[derive(Debug, ThisError, AsRefStr)] pub enum GetSessionError { #[error("Request completed successfully but no credentials were returned")] EmptyResponse, // SDK returned successfully but credentials are None #[error("Error response from AWS SDK: {0}")] SdkError(#[from] AwsSdkError), #[error("Could not construct session: credentials are locked")] CredentialsLocked, #[error("Could not construct session: no credentials are known")] CredentialsEmpty, } #[derive(Debug, ThisError, AsRefStr)] pub enum UnlockError { #[error("App is not locked")] NotLocked, #[error("No saved credentials were found")] NoCredentials, #[error(transparent)] Crypto(#[from] CryptoError), #[error("Data was found to be corrupt after decryption")] InvalidUtf8, // Somehow we got invalid utf-8 even though decryption succeeded #[error("Database error: {0}")] DbError(#[from] SqlxError), #[error("Failed to create AWS session: {0}")] GetSession(#[from] GetSessionError), } #[derive(Debug, ThisError, AsRefStr)] pub enum LockError { #[error("App is not unlocked")] NotUnlocked, #[error(transparent)] LoadCredentials(#[from] LoadCredentialsError), #[error(transparent)] Setup(#[from] SetupError), #[error(transparent)] TauriError(#[from] tauri::Error), #[error(transparent)] Crypto(#[from] CryptoError), } #[derive(Debug, ThisError, AsRefStr)] pub enum SaveCredentialsError { #[error("Database error: {0}")] DbError(#[from] SqlxError), #[error("Encryption error: {0}")] Crypto(#[from] CryptoError), #[error(transparent)] Session(#[from] GetCredentialsError), #[error("App is locked")] Locked, #[error("Credential is temporary and cannot be saved")] NotPersistent, #[error("A credential with that name already exists")] Duplicate, // rekeying is fundamentally a save operation, // but involves loading in order to re-save #[error(transparent)] LoadCredentials(#[from] LoadCredentialsError), } #[derive(Debug, ThisError, AsRefStr)] pub enum LoadCredentialsError { #[error("Database error: {0}")] DbError(#[from] SqlxError), #[error("Invalid passphrase")] // pretty sure this is the only way decryption fails Encryption(#[from] CryptoError), #[error("Credentials not found")] NoCredentials, #[error("Could not decode credential data")] InvalidData, #[error(transparent)] LoadKv(#[from] LoadKvError), } #[derive(Debug, ThisError, AsRefStr)] pub enum LoadKvError { #[error("Database error: {0}")] DbError(#[from] SqlxError), #[error("Could not parse value from database: {0}")] Invalid(#[from] serde_json::Error), } #[derive(Debug, ThisError, AsRefStr)] pub enum CryptoError { #[error(transparent)] Argon2(#[from] argon2::Error), #[error("Invalid passphrase")] // I think this is the only way decryption fails Aead(#[from] chacha20poly1305::aead::Error), #[error("App is currently locked")] Locked, #[error("No passphrase has been specified")] Empty, } // Errors encountered while trying to figure out who's on the other end of a request #[derive(Debug, ThisError, AsRefStr)] pub enum ClientInfoError { #[error("Found PID for client socket, but no corresponding process")] ProcessNotFound, #[error("Could not determine parent PID of connected client")] ParentPidNotFound, #[error("Found PID for parent process of client, but no corresponding process")] ParentProcessNotFound, #[cfg(windows)] #[error("Could not determine PID of connected client")] WindowsError(#[from] windows::core::Error), #[error(transparent)] Io(#[from] std::io::Error), } // Technically also an error, but formatted as a struct for easy deserialization #[derive(Debug, Serialize, Deserialize)] pub struct ServerError { code: String, msg: String, } impl std::fmt::Display for ServerError { fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { write!(f, "{} ({})", self.msg, self.code)?; Ok(()) } } // Errors encountered while requesting credentials via CLI (creddy show, creddy exec) #[derive(Debug, ThisError, AsRefStr)] pub enum RequestError { #[error("Error response from server: {0}")] Server(ServerError), #[error("Unexpected response from server")] Unexpected(crate::server::Response), #[error("The server did not respond with valid JSON")] InvalidJson(#[from] serde_json::Error), #[error("Error reading/writing stream: {0}")] StreamIOError(#[from] std::io::Error), } impl From for RequestError { fn from(s: ServerError) -> Self { Self::Server(s) } } #[derive(Debug, ThisError, AsRefStr)] pub enum CliError { #[error(transparent)] Request(#[from] RequestError), #[error(transparent)] Exec(#[from] ExecError), #[error(transparent)] Io(#[from] std::io::Error), } // Errors encountered while trying to launch a child process #[derive(Debug, ThisError, AsRefStr)] pub enum ExecError { #[error("Please specify a command")] NoCommand, #[error("Executable not found: {0:?}")] NotFound(OsString), #[error("Failed to execute command: {0}")] ExecutionFailed(#[from] std::io::Error), #[error(transparent)] GetCredentials(#[from] GetCredentialsError), } #[derive(Debug, ThisError, AsRefStr)] pub enum LaunchTerminalError { #[error("Could not discover main window")] NoMainWindow, #[error("Failed to communicate with main Creddy window")] IpcFailed(#[from] tauri::Error), #[error("Failed to launch terminal: {0}")] Exec(#[from] ExecError), #[error(transparent)] GetCredentials(#[from] GetCredentialsError), } // ========================= // Serialize implementations // ========================= struct SerializeWrapper(pub E); impl Serialize for SerializeWrapper<&GetSessionTokenError> { fn serialize(&self, serializer: S) -> Result { let err = self.0; let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", &err.code())?; map.serialize_entry("msg", &err.message())?; map.serialize_entry("source", &None::<&str>)?; map.end() } } impl_serialize_basic!(SetupError); impl_serialize_basic!(GetCredentialsError); impl_serialize_basic!(ClientInfoError); impl_serialize_basic!(WindowError); impl_serialize_basic!(LockError); impl_serialize_basic!(SaveCredentialsError); impl_serialize_basic!(LoadCredentialsError); impl Serialize for HandlerError { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; map.end() } } impl Serialize for SendResponseError { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; match self { SendResponseError::SessionRenew(src) => map.serialize_entry("source", &src)?, _ => serialize_upstream_err(self, &mut map)?, } map.end() } } impl Serialize for GetSessionError { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; match self { GetSessionError::SdkError(AwsSdkError::ServiceError(se_wrapper)) => { let err = se_wrapper.err(); map.serialize_entry("source", &SerializeWrapper(err))? } _ => serialize_upstream_err(self, &mut map)?, } map.end() } } impl Serialize for UnlockError { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; match self { UnlockError::GetSession(src) => map.serialize_entry("source", &src)?, // The string representation of the AEAD error is not very helpful, so skip it UnlockError::Crypto(_src) => map.serialize_entry("source", &None::<&str>)?, _ => serialize_upstream_err(self, &mut map)?, } map.end() } } impl Serialize for ExecError { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; match self { ExecError::GetCredentials(src) => map.serialize_entry("source", &src)?, _ => serialize_upstream_err(self, &mut map)?, } map.end() } } impl Serialize for LaunchTerminalError { fn serialize(&self, serializer: S) -> Result { let mut map = serializer.serialize_map(None)?; map.serialize_entry("code", self.as_ref())?; map.serialize_entry("msg", &format!("{self}"))?; match self { LaunchTerminalError::Exec(src) => map.serialize_entry("source", &src)?, _ => serialize_upstream_err(self, &mut map)?, } map.end() } }