use std::error::Error; use std::convert::AsRef; use std::ffi::OsString; use std::sync::mpsc; use std::string::FromUtf8Error; use strum_macros::AsRefStr; use thiserror::Error as ThisError; use aws_sdk_sts::{ types::SdkError as AwsSdkError, error::GetSessionTokenError, }; use sqlx::{ error::Error as SqlxError, migrate::MigrateError, }; use tauri::api::dialog::{ MessageDialogBuilder, MessageDialogKind, }; use serde::{ Serialize, Serializer, ser::SerializeMap, Deserialize, }; pub trait ShowError { fn error_popup(self, title: &str); fn error_popup_nowait(self, title: &str); fn error_print(self); fn error_print_prefix(self, prefix: &str); } impl ShowError for Result<(), E> { fn error_popup(self, title: &str) { if let Err(e) = self { let (tx, rx) = mpsc::channel(); MessageDialogBuilder::new(title, format!("{e}")) .kind(MessageDialogKind::Error) .show(move |_| tx.send(true).unwrap()); rx.recv().unwrap(); } } fn error_popup_nowait(self, title: &str) { if let Err(e) = self { MessageDialogBuilder::new(title, format!("{e}")) .kind(MessageDialogKind::Error) .show(|_| {}) } } 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() } 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>)?; 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 running migrations: {0}")] MigrationError(#[from] MigrateError), #[error("Error parsing configuration from database")] ConfigParseError(#[from] serde_json::Error), #[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] tauri::Error), } #[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 an HTTP 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("Internal server error")] Internal, #[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, } #[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 construt 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 CryptoError { #[error(transparent)] Argon2(#[from] argon2::Error), #[error("Invalid passphrase")] // I think this is the only way decryption fails Aead(#[from] chacha20poly1305::aead::Error), } // 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 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() } }