diff --git a/src-tauri/creddy_cli/src/cli/docker.rs b/src-tauri/creddy_cli/src/cli/docker.rs index f4a55de..7dbb010 100644 --- a/src-tauri/creddy_cli/src/cli/docker.rs +++ b/src-tauri/creddy_cli/src/cli/docker.rs @@ -1,4 +1,4 @@ -use std::io; +use std::io::{self, Read}; use anyhow::bail; @@ -12,7 +12,6 @@ use super::{ pub fn docker_store(global_args: GlobalArgs) -> anyhow::Result<()> { let input: DockerCredential = serde_json::from_reader(io::stdin())?; - dbg!(&input); let req = CliRequest::SaveCredential { name: input.username.clone(), @@ -25,3 +24,20 @@ pub fn docker_store(global_args: GlobalArgs) -> anyhow::Result<()> { r => bail!("Unexpected response from server: {r}"), } } + + +pub fn docker_get(global_args: GlobalArgs) -> anyhow::Result<()> { + let mut server_url = String::new(); + io::stdin().read_to_string(&mut server_url)?; + let req = CliRequest::GetDockerCredential { + server_url: server_url.trim().to_owned() + }; + + match super::make_request(global_args.server_addr, &req)?? { + CliResponse::Credential(CliCredential::Docker(d)) => { + println!("{}", serde_json::to_string(&d)?); + }, + r => bail!("Unexpected response from server: {r}"), + } + Ok(()) +} diff --git a/src-tauri/creddy_cli/src/cli/mod.rs b/src-tauri/creddy_cli/src/cli/mod.rs index f567a65..bd9faf6 100644 --- a/src-tauri/creddy_cli/src/cli/mod.rs +++ b/src-tauri/creddy_cli/src/cli/mod.rs @@ -118,7 +118,7 @@ pub enum DockerCmd { pub fn get(args: GetArgs, global: GlobalArgs) -> anyhow::Result<()> { - let req = CliRequest::GetCredential { + let req = CliRequest::GetAwsCredential { name: args.name, base: args.base, }; @@ -145,7 +145,7 @@ pub fn exec(args: ExecArgs, global: GlobalArgs) -> anyhow::Result<()> { let mut cmd = ChildCommand::new(cmd_name); cmd.args(cmd_line); - let req = CliRequest::GetCredential { + let req = CliRequest::GetAwsCredential { name: args.get_args.name, base: args.get_args.base, }; @@ -203,7 +203,7 @@ pub fn invoke_shortcut(args: InvokeArgs, global: GlobalArgs) -> anyhow::Result<( pub fn docker_credential_helper(cmd: DockerCmd, global_args: GlobalArgs) -> anyhow::Result<()> { match cmd { - DockerCmd::Get => todo!(), + DockerCmd::Get => docker::docker_get(global_args), DockerCmd::Store => docker::docker_store(global_args), DockerCmd::Erase => todo!(), } diff --git a/src-tauri/creddy_cli/src/proto.rs b/src-tauri/creddy_cli/src/proto.rs index 37ffece..9897f86 100644 --- a/src-tauri/creddy_cli/src/proto.rs +++ b/src-tauri/creddy_cli/src/proto.rs @@ -10,10 +10,13 @@ use serde::{Serialize, Deserialize}; #[derive(Debug, Serialize, Deserialize)] pub enum CliRequest { - GetCredential { + GetAwsCredential { name: Option, base: bool, }, + GetDockerCredential { + server_url: String, + }, SaveCredential { name: String, is_default: bool, diff --git a/src-tauri/src/credentials/docker.rs b/src-tauri/src/credentials/docker.rs index c03a576..4e073b1 100644 --- a/src-tauri/src/credentials/docker.rs +++ b/src-tauri/src/credentials/docker.rs @@ -31,7 +31,6 @@ pub struct DockerCredential { pub secret: String, } - impl PersistentCredential for DockerCredential { type Row = DockerRow; diff --git a/src-tauri/src/credentials/mod.rs b/src-tauri/src/credentials/mod.rs index a4ae4d6..68d932d 100644 --- a/src-tauri/src/credentials/mod.rs +++ b/src-tauri/src/credentials/mod.rs @@ -83,6 +83,23 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized { Self::from_row(row, crypto) } + async fn load_by(column: &str, value: T, crypto: &Crypto, pool: &SqlitePool) -> Result + where T: Send + for<'q> sqlx::Encode<'q, Sqlite> + sqlx::Type + { + let query = format!( + "SELECT * FROM {} where {} = ?", + Self::table_name(), + column, + ); + let row: Self::Row = sqlx::query_as(&query) + .bind(value) + .fetch_optional(pool) + .await? + .ok_or(LoadCredentialsError::NoCredentials)?; + + Self::from_row(row, crypto) + } + async fn load_default(crypto: &Crypto, pool: &SqlitePool) -> Result { let q = format!( "SELECT details.* diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index 559819e..c84076f 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -173,7 +173,7 @@ pub enum HandlerError { StreamIOError(#[from] std::io::Error), #[error("Received invalid UTF-8 in request")] InvalidUtf8(#[from] FromUtf8Error), - #[error("HTTP request malformed")] + #[error("Request malformed: {0}")] BadRequest(#[from] serde_json::Error), #[error("HTTP request too large")] RequestTooLarge, @@ -183,6 +183,8 @@ pub enum HandlerError { 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}")] diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index 6159654..663129e 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -16,7 +16,6 @@ use crate::terminal; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AwsRequestNotification { - pub id: u64, pub client: Client, pub name: Option, pub base: bool, @@ -25,27 +24,46 @@ pub struct AwsRequestNotification { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct SshRequestNotification { - pub id: u64, pub client: Client, pub key_name: String, } #[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum RequestNotification { - Aws(AwsRequestNotification), - Ssh(SshRequestNotification), +pub struct DockerRequestNotification { + pub client: Client, + pub server_url: String, } -impl RequestNotification { - pub fn new_aws(id: u64, client: Client, name: Option, base: bool) -> Self { - Self::Aws(AwsRequestNotification {id, client, name, base}) + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum RequestNotificationDetail { + Aws(AwsRequestNotification), + Ssh(SshRequestNotification), + Docker(DockerRequestNotification), +} + +impl RequestNotificationDetail { + pub fn new_aws(client: Client, name: Option, base: bool) -> Self { + Self::Aws(AwsRequestNotification {client, name, base}) } - pub fn new_ssh(id: u64, client: Client, key_name: String) -> Self { - Self::Ssh(SshRequestNotification {id, client, key_name}) + pub fn new_ssh(client: Client, key_name: String) -> Self { + Self::Ssh(SshRequestNotification {client, key_name}) } + + pub fn new_docker(client: Client, server_url: String) -> Self { + Self::Docker(DockerRequestNotification {client, server_url}) + } +} + + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RequestNotification { + pub id: u64, + #[serde(flatten)] + pub detail: RequestNotificationDetail, } diff --git a/src-tauri/src/srv/agent.rs b/src-tauri/src/srv/agent.rs index d046027..c76baae 100644 --- a/src-tauri/src/srv/agent.rs +++ b/src-tauri/src/srv/agent.rs @@ -11,7 +11,7 @@ use tokio_util::codec::Framed; use crate::clientinfo; use crate::errors::*; -use crate::ipc::{Approval, RequestNotification}; +use crate::ipc::{Approval, RequestNotification, RequestNotificationDetail}; use crate::state::AppState; use super::{CloseWaiter, Stream}; @@ -40,7 +40,7 @@ async fn handle( // corrupt the framing. Clients don't seem to behave that way though? let waiter = CloseWaiter { stream: adapter.get_mut() }; let resp = sign_request(req, app_handle.clone(), client_pid, waiter).await?; - + // have to do this before we send since we can't inspect the message after let is_failure = matches!(resp, Message::Failure); adapter.send(resp).await?; @@ -69,47 +69,21 @@ async fn sign_request( req: SignRequest, app_handle: AppHandle, client_pid: u32, - mut waiter: CloseWaiter<'_>, + waiter: CloseWaiter<'_>, ) -> Result { let state = app_handle.state::(); - let rehide_ms = { - let config = state.config.read().await; - config.rehide_ms - }; + let client = clientinfo::get_client(client_pid, false)?; - let lease = state.acquire_visibility_lease(rehide_ms).await - .map_err(|_e| HandlerError::NoMainWindow)?; + let key_name = state.ssh_name_from_pubkey(&req.pubkey_blob).await?; + let detail = RequestNotificationDetail::new_ssh(client, key_name.clone()); - let (chan_send, chan_recv) = oneshot::channel(); - let request_id = state.register_request(chan_send).await; - - let proceed = async { - let key_name = state.ssh_name_from_pubkey(&req.pubkey_blob).await?; - let notification = RequestNotification::new_ssh(request_id, client, key_name.clone()); - app_handle.emit("credential-request", ¬ification)?; - - let response = tokio::select! { - r = chan_recv => r?, - _ = waiter.wait_for_close() => { - app_handle.emit("request-cancelled", request_id)?; - return Err(HandlerError::Abandoned); - }, - }; - - if let Approval::Denied = response.approval { - return Ok(Message::Failure); - } - - let key = state.sshkey_by_name(&key_name).await?; - let sig = key.sign_request(&req)?; - Ok(Message::SignResponse(sig)) - }; - - let res = proceed.await; - if let Err(_) = &res { - state.unregister_request(request_id).await; + let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; + match response.approval { + Approval::Approved => { + let key = state.sshkey_by_name(&key_name).await?; + let sig = key.sign_request(&req)?; + Ok(Message::SignResponse(sig)) + }, + Approval::Denied => Err(HandlerError::Abandoned), } - - lease.release(); - res } diff --git a/src-tauri/src/srv/creddy_server.rs b/src-tauri/src/srv/creddy_server.rs index aa10f88..6cb6817 100644 --- a/src-tauri/src/srv/creddy_server.rs +++ b/src-tauri/src/srv/creddy_server.rs @@ -1,10 +1,16 @@ +use sqlx::types::uuid::Uuid; use tauri::{AppHandle, Manager}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::sync::oneshot; use crate::clientinfo::{self, Client}; +use crate::credentials::{ + Credential, + CredentialRecord, + Crypto +}; use crate::errors::*; -use crate::ipc::{Approval, RequestNotification}; +use crate::ipc::{Approval, AwsRequestNotification, RequestNotificationDetail, RequestResponse}; use crate::shortcuts::{self, ShortcutAction}; use crate::state::AppState; use super::{ @@ -46,9 +52,15 @@ async fn handle( let req: CliRequest = serde_json::from_slice(&buf)?; let res = match req { - CliRequest::GetCredential{ name, base } => get_aws_credentials( + CliRequest::GetAwsCredential{ name, base } => get_aws_credentials( name, base, client, app_handle, waiter ).await, + CliRequest::GetDockerCredential{ server_url } => get_docker_credentials ( + server_url, client, app_handle, waiter + ).await, + CliRequest::SaveCredential{ name, is_default, credential } => save_credential( + name, is_default, credential, app_handle + ).await, CliRequest::InvokeShortcut(action) => invoke_shortcut(action).await, }; @@ -74,59 +86,64 @@ async fn get_aws_credentials( base: bool, client: Client, app_handle: AppHandle, - mut waiter: CloseWaiter<'_>, + waiter: CloseWaiter<'_>, +) -> Result { + let detail = RequestNotificationDetail::new_aws(client, name.clone(), base); + let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; + match response.approval { + Approval::Approved => { + let state = app_handle.state::(); + if response.base { + let creds = state.get_aws_base(name).await?; + Ok(CliResponse::Credential(CliCredential::AwsBase(creds))) + } + else { + let creds = state.get_aws_session(name).await?.clone(); + Ok(CliResponse::Credential(CliCredential::AwsSession(creds))) + } + }, + Approval::Denied => Err(HandlerError::Denied), + } +} + +async fn get_docker_credentials( + server_url: String, + client: Client, + app_handle: AppHandle, + waiter: CloseWaiter<'_>, +) -> Result { + let detail = RequestNotificationDetail::new_docker(client, server_url.clone()); + let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; + match response.approval { + Approval::Approved => { + let state = app_handle.state::(); + let creds = state.get_docker_credential(&server_url).await?; + Ok(CliResponse::Credential(CliCredential::Docker(creds))) + }, + Approval::Denied => { + Err(HandlerError::Denied) + }, + } +} + +pub async fn save_credential( + name: String, + is_default: bool, + credential: Credential, + app_handle: AppHandle, ) -> Result { let state = app_handle.state::(); - let rehide_ms = { - let config = state.config.read().await; - config.rehide_ms + + // eventually ask the frontend to unlock here + + // a bit weird but convenient + let random_bytes = Crypto::salt(); + let id = Uuid::from_slice(&random_bytes[..16]).unwrap(); + + let record = CredentialRecord { + id, name, is_default, credential }; - let lease = state.acquire_visibility_lease(rehide_ms).await - .map_err(|_e| HandlerError::NoMainWindow)?; // automate this conversion eventually? + state.save_credential(record).await?; - let (chan_send, chan_recv) = oneshot::channel(); - let request_id = state.register_request(chan_send).await; - - // if an error occurs in any of the following, we want to abort the operation - // but ? returns immediately, and we want to unregister the request before returning - // so we bundle it all up in an async block and return a Result so we can handle errors - let proceed = async { - let notification = RequestNotification::new_aws( - request_id, client, name.clone(), base - ); - app_handle.emit("credential-request", ¬ification)?; - - let response = tokio::select! { - r = chan_recv => r?, - _ = waiter.wait_for_close() => { - app_handle.emit("request-cancelled", request_id)?; - return Err(HandlerError::Abandoned); - }, - }; - - match response.approval { - Approval::Approved => { - if response.base { - let creds = state.get_aws_base(name).await?; - Ok(CliResponse::Credential(CliCredential::AwsBase(creds))) - } - else { - let creds = state.get_aws_session(name).await?.clone(); - Ok(CliResponse::Credential(CliCredential::AwsSession(creds))) - } - }, - Approval::Denied => Err(HandlerError::Denied), - } - }; - - let result = match proceed.await { - Ok(r) => Ok(r), - Err(e) => { - state.unregister_request(request_id).await; - Err(e) - }, - }; - - lease.release(); - result + Ok(CliResponse::Empty) } diff --git a/src-tauri/src/srv/mod.rs b/src-tauri/src/srv/mod.rs index e033b62..06e7cc0 100644 --- a/src-tauri/src/srv/mod.rs +++ b/src-tauri/src/srv/mod.rs @@ -3,13 +3,23 @@ use std::future::Future; use tauri::{ AppHandle, async_runtime as rt, + Manager, }; use tokio::io::AsyncReadExt; +use tokio::sync::oneshot; use serde::{Serialize, Deserialize}; -use crate::credentials::{AwsBaseCredential, AwsSessionCredential}; +use crate::clientinfo::Client; +use crate::credentials::{ + AwsBaseCredential, + AwsSessionCredential, + Credential, + DockerCredential, +}; use crate::errors::*; +use crate::ipc::{RequestNotification, RequestNotificationDetail, RequestResponse}; use crate::shortcuts::ShortcutAction; +use crate::state::AppState; pub mod creddy_server; pub mod agent; @@ -21,10 +31,18 @@ use platform::Stream; // that would make it impossible to build a completely static-linked version #[derive(Debug, Serialize, Deserialize)] pub enum CliRequest { - GetCredential { + GetAwsCredential { name: Option, base: bool, }, + GetDockerCredential { + server_url: String, + }, + SaveCredential { + name: String, + is_default: bool, + credential: Credential, + }, InvokeShortcut(ShortcutAction), } @@ -40,6 +58,7 @@ pub enum CliResponse { pub enum CliCredential { AwsBase(AwsBaseCredential), AwsSession(AwsSessionCredential), + Docker(DockerCredential), } @@ -87,6 +106,48 @@ fn serve(sock_name: &str, app_handle: AppHandle, handler: H) -> std::io::R } +async fn send_credentials_request( + detail: RequestNotificationDetail, + app_handle: AppHandle, + mut waiter: CloseWaiter<'_> +) -> Result { + let state = app_handle.state::(); + let rehide_ms = { + let config = state.config.read().await; + config.rehide_ms + }; + + let lease = state.acquire_visibility_lease(rehide_ms).await + .map_err(|_e| HandlerError::NoMainWindow)?; + + let (chan_send, chan_recv) = oneshot::channel(); + let request_id = state.register_request(chan_send).await; + let notification = RequestNotification { id: request_id, detail }; + + // the following could fail in various ways, but we want to make sure + // the request gets unregistered on any failure, so we wrap this all + // up in an async block so that we only have to handle the error case once + let proceed = async { + app_handle.emit("credential-request", ¬ification)?; + tokio::select! { + r = chan_recv => Ok(r?), + _ = waiter.wait_for_close() => { + app_handle.emit("request-cancelled", request_id)?; + Err(HandlerError::Abandoned) + }, + } + }; + + let res = proceed.await; + if let Err(_) = &res { + state.unregister_request(request_id).await; + } + + lease.release(); + res +} + + #[cfg(unix)] mod platform { use std::io::ErrorKind; diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 5e5f535..de2bfb3 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -19,6 +19,7 @@ use crate::app; use crate::credentials::{ AppSession, AwsSessionCredential, + DockerCredential, SshKey, }; use crate::{config, config::AppConfig}; @@ -193,7 +194,7 @@ impl AppState { pub async fn update_config(&self, new_config: AppConfig) -> Result<(), SetupError> { let mut live_config = self.config.write().await; - + // update autostart if necessary if new_config.start_on_login != live_config.start_on_login { config::set_auto_launch(new_config.start_on_login)?; @@ -322,6 +323,13 @@ impl AppState { Ok(k) } + pub async fn get_docker_credential(&self, server_url: &str) -> Result { + let app_session = self.app_session.read().await; + let crypto = app_session.try_get_crypto()?; + let d = DockerCredential::load_by("server_url", server_url.to_owned(), crypto, &self.pool).await?; + Ok(d) + } + pub async fn signal_activity(&self) { let mut last_activity = self.last_activity.write().await; *last_activity = OffsetDateTime::now_utc(); diff --git a/src/views/approve/CollectResponse.svelte b/src/views/approve/CollectResponse.svelte index 4b6cc06..729243a 100644 --- a/src/views/approve/CollectResponse.svelte +++ b/src/views/approve/CollectResponse.svelte @@ -34,7 +34,7 @@
- WARNING: This application is requesting your base AWS credentials. + WARNING: This application is requesting your base AWS credentials. These credentials are less secure than session credentials, since they don't expire automatically.
@@ -51,6 +51,8 @@ {/if} {:else if $appState.currentRequest.type === 'Ssh'} {appName ? `"${appName}"` : 'An application'} would like to use your SSH key "{$appState.currentRequest.key_name}". + {:else if $appState.currentRequest.type === 'Docker'} + {appName ? `"${appName}"` : 'An application'} would like to use your Docker credentials for {$appState.currentRequest.server_url}. {/if}