561 lines
16 KiB
Rust
561 lines
16 KiB
Rust
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<T, E>
|
|
{
|
|
fn error_popup(self, title: &str);
|
|
fn error_print(self);
|
|
fn error_print_prefix(self, prefix: &str);
|
|
}
|
|
|
|
impl<T, E> ShowError<T, E> for Result<T, E>
|
|
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<E, S>(err: &E, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
E: std::error::Error + AsRef<str>,
|
|
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<E>(pub E);
|
|
|
|
impl<E: Error> Serialize for SerializeUpstream<E> {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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<E, M>(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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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("Request malformed: {0}")]
|
|
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 saving credentials: {0}")]
|
|
SaveCredentials(#[from] SaveCredentialsError),
|
|
#[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,
|
|
#[error(transparent)]
|
|
SshAgent(#[from] ssh_agent_lib::error::AgentError),
|
|
#[error(transparent)]
|
|
SshKey(#[from] ssh_key::Error),
|
|
#[error(transparent)]
|
|
Signature(#[from] signature::Error),
|
|
#[error(transparent)]
|
|
Encoding(#[from] ssh_encoding::Error),
|
|
|
|
#[cfg(windows)]
|
|
#[error(transparent)]
|
|
Windows(#[from] windows::core::Error),
|
|
}
|
|
|
|
|
|
#[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<GetSessionTokenError>),
|
|
#[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,
|
|
#[error("Failed to save credentials: {0}")]
|
|
Encode(#[from] ssh_key::Error),
|
|
// 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("Could not determine PID of connected client")]
|
|
PidNotFound,
|
|
#[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::srv::CliResponse),
|
|
#[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<ServerError> 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),
|
|
}
|
|
|
|
|
|
#[derive(Debug, ThisError, AsRefStr)]
|
|
pub enum LoadSshKeyError {
|
|
#[error("Passphrase is invalid")]
|
|
InvalidPassphrase,
|
|
#[error("Could not parse SSH private key data")]
|
|
InvalidData(#[from] ssh_key::Error),
|
|
#[error(transparent)]
|
|
Io(#[from] std::io::Error),
|
|
}
|
|
|
|
|
|
// =========================
|
|
// Serialize implementations
|
|
// =========================
|
|
|
|
|
|
struct SerializeWrapper<E>(pub E);
|
|
|
|
impl Serialize for SerializeWrapper<&GetSessionTokenError> {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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_basic!(LoadSshKeyError);
|
|
|
|
|
|
impl Serialize for HandlerError {
|
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
|
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()
|
|
}
|
|
}
|