finish SSH key support

This commit is contained in:
Joseph Montanaro 2024-07-03 14:54:10 -04:00
parent 00089d7efb
commit 9fd355b68e
17 changed files with 314 additions and 372 deletions

View File

@ -21,7 +21,7 @@ use crate::{
config::{self, AppConfig}, config::{self, AppConfig},
credentials::AppSession, credentials::AppSession,
ipc, ipc,
server::{Server, Agent}, srv::{creddy_server, agent},
errors::*, errors::*,
shortcuts, shortcuts,
state::AppState, state::AppState,
@ -105,8 +105,8 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
}; };
let app_session = AppSession::load(&pool).await?; let app_session = AppSession::load(&pool).await?;
Server::start(app.handle().clone())?; creddy_server::serve(app.handle().clone())?;
Agent::start(app.handle().clone())?; agent::serve(app.handle().clone())?;
config::set_auto_launch(conf.start_on_login)?; config::set_auto_launch(conf.start_on_login)?;
if let Err(_e) = config::set_auto_launch(conf.start_on_login) { if let Err(_e) = config::set_auto_launch(conf.start_on_login) {

View File

@ -13,7 +13,11 @@ use clap::{
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::errors::*; use crate::errors::*;
use crate::server::{Request, Response}; use crate::srv::{
self,
Request,
Response
};
use crate::shortcuts::ShortcutAction; use crate::shortcuts::ShortcutAction;
#[cfg(unix)] #[cfg(unix)]
@ -47,6 +51,10 @@ pub fn parser() -> Command<'static> {
.action(ArgAction::SetTrue) .action(ArgAction::SetTrue)
.help("Use base credentials instead of session credentials") .help("Use base credentials instead of session credentials")
) )
.arg(
Arg::new("name")
.help("If unspecified, use default credentials")
)
) )
.subcommand( .subcommand(
Command::new("exec") Command::new("exec")
@ -59,6 +67,12 @@ pub fn parser() -> Command<'static> {
.action(ArgAction::SetTrue) .action(ArgAction::SetTrue)
.help("Use base credentials instead of session credentials") .help("Use base credentials instead of session credentials")
) )
.arg(
Arg::new("name")
.short('n')
.long("name")
.help("If unspecified, use default credentials")
)
.arg( .arg(
Arg::new("command") Arg::new("command")
.multiple_values(true) .multiple_values(true)
@ -78,8 +92,10 @@ pub fn parser() -> Command<'static> {
pub fn get(args: &ArgMatches) -> Result<(), CliError> { pub fn get(args: &ArgMatches) -> Result<(), CliError> {
let base = args.get_one("base").unwrap_or(&false); let name = args.get_one("name").cloned();
let output = match make_request(&Request::GetAwsCredentials { base: *base })? { let base = *args.get_one("base").unwrap_or(&false);
let output = match make_request(&Request::GetAwsCredentials { name, base })? {
Response::AwsBase(creds) => serde_json::to_string(&creds).unwrap(), Response::AwsBase(creds) => serde_json::to_string(&creds).unwrap(),
Response::AwsSession(creds) => serde_json::to_string(&creds).unwrap(), Response::AwsSession(creds) => serde_json::to_string(&creds).unwrap(),
r => return Err(RequestError::Unexpected(r).into()), r => return Err(RequestError::Unexpected(r).into()),
@ -90,6 +106,7 @@ pub fn get(args: &ArgMatches) -> Result<(), CliError> {
pub fn exec(args: &ArgMatches) -> Result<(), CliError> { pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
let name = args.get_one("name").cloned();
let base = *args.get_one("base").unwrap_or(&false); let base = *args.get_one("base").unwrap_or(&false);
let mut cmd_line = args.get_many("command") let mut cmd_line = args.get_many("command")
.ok_or(ExecError::NoCommand)?; .ok_or(ExecError::NoCommand)?;
@ -98,7 +115,7 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
let mut cmd = ChildCommand::new(cmd_name); let mut cmd = ChildCommand::new(cmd_name);
cmd.args(cmd_line); cmd.args(cmd_line);
match make_request(&Request::GetAwsCredentials { base })? { match make_request(&Request::GetAwsCredentials { name, base })? {
Response::AwsBase(creds) => { Response::AwsBase(creds) => {
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id); cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key); cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
@ -178,7 +195,8 @@ async fn make_request(req: &Request) -> Result<Response, RequestError> {
async fn connect() -> Result<NamedPipeClient, std::io::Error> { async fn connect() -> Result<NamedPipeClient, std::io::Error> {
// apparently attempting to connect can fail if there's already a client connected // apparently attempting to connect can fail if there's already a client connected
loop { loop {
match ClientOptions::new().open(r"\\.\pipe\creddy-requests") { let addr = srv::addr("creddy-server");
match ClientOptions::new().open(&addr) {
Ok(stream) => return Ok(stream), Ok(stream) => return Ok(stream),
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => (), Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => (),
Err(e) => return Err(e), Err(e) => return Err(e),
@ -190,5 +208,6 @@ async fn connect() -> Result<NamedPipeClient, std::io::Error> {
#[cfg(unix)] #[cfg(unix)]
async fn connect() -> Result<UnixStream, std::io::Error> { async fn connect() -> Result<UnixStream, std::io::Error> {
UnixStream::connect("/tmp/creddy.sock").await let path = srv::addr("creddy-server");
UnixStream::connect(&path).await
} }

View File

@ -122,6 +122,16 @@ impl CredentialRecord {
// Self::load_credential(row, crypto, pool).await // Self::load_credential(row, crypto, pool).await
// } // }
pub async fn load_by_name(name: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE name = ?")
.bind(name)
.fetch_optional(pool)
.await?
.ok_or(LoadCredentialsError::NoCredentials)?;
Self::load_credential(row, crypto, pool).await
}
pub async fn load_default(credential_type: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> { pub async fn load_default(credential_type: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
let row: CredentialRow = sqlx::query_as( let row: CredentialRow = sqlx::query_as(
"SELECT * FROM credentials "SELECT * FROM credentials

View File

@ -338,6 +338,8 @@ pub enum ClientInfoError {
#[cfg(windows)] #[cfg(windows)]
#[error("Could not determine PID of connected client")] #[error("Could not determine PID of connected client")]
WindowsError(#[from] windows::core::Error), WindowsError(#[from] windows::core::Error),
#[error("Could not determine PID of connected client")]
PidNotFound,
#[error(transparent)] #[error(transparent)]
Io(#[from] std::io::Error), Io(#[from] std::io::Error),
} }
@ -364,7 +366,7 @@ pub enum RequestError {
#[error("Error response from server: {0}")] #[error("Error response from server: {0}")]
Server(ServerError), Server(ServerError),
#[error("Unexpected response from server")] #[error("Unexpected response from server")]
Unexpected(crate::server::Response), Unexpected(crate::srv::Response),
#[error("The server did not respond with valid JSON")] #[error("The server did not respond with valid JSON")]
InvalidJson(#[from] serde_json::Error), InvalidJson(#[from] serde_json::Error),
#[error("Error reading/writing stream: {0}")] #[error("Error reading/writing stream: {0}")]

View File

@ -18,6 +18,7 @@ use crate::terminal;
pub struct AwsRequestNotification { pub struct AwsRequestNotification {
pub id: u64, pub id: u64,
pub client: Client, pub client: Client,
pub name: Option<String>,
pub base: bool, pub base: bool,
} }
@ -38,8 +39,8 @@ pub enum RequestNotification {
} }
impl RequestNotification { impl RequestNotification {
pub fn new_aws(id: u64, client: Client, base: bool) -> Self { pub fn new_aws(id: u64, client: Client, name: Option<String>, base: bool) -> Self {
Self::Aws(AwsRequestNotification {id, client, base}) Self::Aws(AwsRequestNotification {id, client, name, base})
} }
pub fn new_ssh(id: u64, client: Client, key_name: String) -> Self { pub fn new_ssh(id: u64, client: Client, key_name: String) -> Self {

View File

@ -7,7 +7,7 @@ mod clientinfo;
mod ipc; mod ipc;
mod kv; mod kv;
mod state; mod state;
pub mod server; mod srv;
mod shortcuts; mod shortcuts;
mod terminal; mod terminal;
mod tray; mod tray;

View File

@ -1,77 +0,0 @@
use signature::Signer;
use ssh_agent_lib::agent::{Agent, Session};
use ssh_agent_lib::proto::message::Message;
use ssh_key::public::PublicKey;
use ssh_key::private::PrivateKey;
use tokio::net::UnixListener;
struct SshAgent;
impl std::default::Default for SshAgent {
fn default() -> Self {
SshAgent {}
}
}
#[ssh_agent_lib::async_trait]
impl Session for SshAgent {
async fn handle(&mut self, message: Message) -> Result<Message, Box<dyn std::error::Error>> {
println!("Received message");
match message {
Message::RequestIdentities => {
let p = std::path::PathBuf::from("/home/joe/.ssh/id_ed25519.pub");
let pubkey = PublicKey::read_openssh_file(&p).unwrap();
let id = ssh_agent_lib::proto::message::Identity {
pubkey_blob: pubkey.to_bytes().unwrap(),
comment: pubkey.comment().to_owned(),
};
Ok(Message::IdentitiesAnswer(vec![id]))
},
Message::SignRequest(req) => {
println!("Received sign request");
let mut req_bytes = vec![13];
encode_string(&mut req_bytes, &req.pubkey_blob);
encode_string(&mut req_bytes, &req.data);
req_bytes.extend(req.flags.to_be_bytes());
std::fs::File::create("/tmp/signreq").unwrap().write(&req_bytes).unwrap();
let p = std::path::PathBuf::from("/home/joe/.ssh/id_ed25519");
let passphrase = std::env::var("PRIVKEY_PASSPHRASE").unwrap();
let privkey = PrivateKey::read_openssh_file(&p)
.unwrap()
.decrypt(passphrase.as_bytes())
.unwrap();
let sig = Signer::sign(&privkey, &req.data);
use std::io::Write;
std::fs::File::create("/tmp/sig").unwrap().write(sig.as_bytes()).unwrap();
let mut payload = Vec::with_capacity(128);
encode_string(&mut payload, "ssh-ed25519".as_bytes());
encode_string(&mut payload, sig.as_bytes());
println!("Payload length: {}", payload.len());
std::fs::File::create("/tmp/payload").unwrap().write(&payload).unwrap();
Ok(Message::SignResponse(payload))
},
_ => Ok(Message::Failure),
}
}
}
fn encode_string(buf: &mut Vec<u8>, s: &[u8]) {
let len = s.len() as u32;
buf.extend(len.to_be_bytes());
buf.extend(s);
}
pub async fn run() {
let socket = "/tmp/creddy-agent.sock";
let _ = std::fs::remove_file(socket);
let listener = UnixListener::bind(socket).unwrap();
SshAgent.listen(listener).await.unwrap();
}

View File

@ -1,58 +0,0 @@
use std::io::ErrorKind;
use tokio::net::{UnixListener, UnixStream};
use tauri::{
AppHandle,
async_runtime as rt,
};
use crate::errors::*;
pub type Stream = UnixStream;
pub struct Server {
listener: UnixListener,
app_handle: AppHandle,
}
impl Server {
pub fn start(app_handle: AppHandle) -> std::io::Result<()> {
match std::fs::remove_file("/tmp/creddy.sock") {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::NotFound => (),
Err(e) => return Err(e),
}
let listener = UnixListener::bind("/tmp/creddy.sock")?;
let srv = Server { listener, app_handle };
rt::spawn(srv.serve());
Ok(())
}
async fn serve(self) {
loop {
self.try_serve()
.await
.error_print_prefix("Error accepting request: ");
}
}
async fn try_serve(&self) -> Result<(), HandlerError> {
let (stream, _addr) = self.listener.accept().await?;
let new_handle = self.app_handle.clone();
let client_pid = get_client_pid(&stream)?;
rt::spawn(async move {
super::handle(stream, new_handle, client_pid)
.await
.error_print_prefix("Error responding to request: ");
});
Ok(())
}
}
fn get_client_pid(stream: &UnixStream) -> std::io::Result<u32> {
let cred = stream.peer_cred()?;
Ok(cred.pid().unwrap() as u32)
}

View File

@ -1,74 +0,0 @@
use tokio::net::windows::named_pipe::{
NamedPipeServer,
ServerOptions,
};
use tauri::{AppHandle, Manager};
use windows::Win32:: {
Foundation::HANDLE,
System::Pipes::GetNamedPipeClientProcessId,
};
use std::os::windows::io::AsRawHandle;
use tauri::async_runtime as rt;
use crate::errors::*;
// used by parent module
pub type Stream = NamedPipeServer;
pub struct Server {
listener: NamedPipeServer,
app_handle: AppHandle,
}
impl Server {
pub fn start(app_handle: AppHandle) -> std::io::Result<()> {
let listener = ServerOptions::new()
.first_pipe_instance(true)
.create(r"\\.\pipe\creddy-requests")?;
let srv = Server {listener, app_handle};
rt::spawn(srv.serve());
Ok(())
}
async fn serve(mut self) {
loop {
if let Err(e) = self.try_serve().await {
eprintln!("Error accepting connection: {e}");
}
}
}
async fn try_serve(&mut self) -> Result<(), HandlerError> {
// connect() just waits for a client to connect, it doesn't return anything
self.listener.connect().await?;
// create a new pipe instance to listen for the next client, and swap it in
let new_listener = ServerOptions::new().create(r"\\.\pipe\creddy-requests")?;
let stream = std::mem::replace(&mut self.listener, new_listener);
let new_handle = self.app_handle.clone();
let client_pid = get_client_pid(&stream)?;
rt::spawn(async move {
super::handle(stream, new_handle, client_pid)
.await
.error_print_prefix("Error responding to request: ");
});
Ok(())
}
}
fn get_client_pid(pipe: &NamedPipeServer) -> Result<u32, ClientInfoError> {
let raw_handle = pipe.as_raw_handle();
let mut pid = 0u32;
let handle = HANDLE(raw_handle as _);
unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? };
Ok(pid)
}

View File

@ -1,98 +1,68 @@
use std::io::ErrorKind;
use futures::SinkExt; use futures::SinkExt;
use signature::Signer; use signature::Signer;
use ssh_agent_lib::agent::MessageCodec; use ssh_agent_lib::agent::MessageCodec;
use ssh_agent_lib::proto::message::{ use ssh_agent_lib::proto::message::{
Message, Message,
Identity,
SignRequest, SignRequest,
}; };
use tokio::net::{UnixListener, UnixStream}; use tauri::{AppHandle, Manager};
use tauri::{
AppHandle,
Manager,
async_runtime as rt,
};
use tokio_util::codec::Framed;
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tokio_util::codec::Framed;
use crate::clientinfo; use crate::clientinfo;
use crate::errors::*; use crate::errors::*;
use crate::ipc::{Approval, RequestNotification}; use crate::ipc::{Approval, RequestNotification};
use crate::state::AppState; use crate::state::AppState;
use super::{CloseWaiter, Stream};
pub struct Agent {
listener: UnixListener,
app_handle: AppHandle,
}
impl Agent { pub fn serve(app_handle: AppHandle) -> std::io::Result<()> {
pub fn start(app_handle: AppHandle) -> std::io::Result<()> { super::serve("creddy-agent", app_handle, handle)
match std::fs::remove_file("/tmp/creddy-agent.sock") {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::NotFound => (),
Err(e) => return Err(e),
}
let listener = UnixListener::bind("/tmp/creddy-agent.sock")?;
let srv = Agent { listener, app_handle };
rt::spawn(srv.serve());
Ok(())
}
async fn serve(self) {
loop {
self.try_serve()
.await
.error_print_prefix("Error accepting request: ");
}
}
async fn try_serve(&self) -> Result<(), HandlerError> {
let (stream, _addr) = self.listener.accept().await?;
let new_handle = self.app_handle.clone();
let client_pid = get_client_pid(&stream)?;
rt::spawn(async move {
let adapter = Framed::new(stream, MessageCodec);
handle_framed(adapter, new_handle, client_pid)
.await
.error_print_prefix("Error responding to request: ");
});
Ok(())
}
} }
async fn handle_framed( async fn handle(
mut adapter: Framed<UnixStream, MessageCodec>, stream: Stream,
app_handle: AppHandle, app_handle: AppHandle,
client_pid: u32, client_pid: u32
) -> Result<(), HandlerError> { ) -> Result<(), HandlerError> {
let mut adapter = Framed::new(stream, MessageCodec);
while let Some(message) = adapter.try_next().await? { while let Some(message) = adapter.try_next().await? {
let resp = match message { match message {
Message::RequestIdentities => list_identities(app_handle.clone()).await?, Message::RequestIdentities => {
Message::SignRequest(req) => sign_request(req, app_handle.clone(), client_pid).await?, let resp = list_identities(app_handle.clone()).await?;
_ => Message::Failure, adapter.send(resp).await?;
},
Message::SignRequest(req) => {
// CloseWaiter could corrupt the framing, but this doesn't matter
// since we don't plan to pull any more frames out of the stream
let waiter = CloseWaiter { stream: adapter.get_mut() };
let resp = sign_request(req, app_handle.clone(), client_pid, waiter).await?;
adapter.send(resp).await?;
break;
},
_ => adapter.send(Message::Failure).await?,
}; };
adapter.send(resp).await?;
} }
Ok(()) Ok(())
} }
async fn list_identities(app_handle: AppHandle) -> Result<Message, HandlerError> { async fn list_identities(app_handle: AppHandle) -> Result<Message, HandlerError> {
let state = app_handle.state::<AppState>(); let state = app_handle.state::<AppState>();
let identities: Vec<Identity> = state.list_ssh_identities().await?; let identities = state.list_ssh_identities().await?;
Ok(Message::IdentitiesAnswer(identities)) Ok(Message::IdentitiesAnswer(identities))
} }
async fn sign_request(req: SignRequest, app_handle: AppHandle, client_pid: u32) -> Result<Message, HandlerError> { async fn sign_request(
req: SignRequest,
app_handle: AppHandle,
client_pid: u32,
mut waiter: CloseWaiter<'_>,
) -> Result<Message, HandlerError> {
let state = app_handle.state::<AppState>(); let state = app_handle.state::<AppState>();
let rehide_ms = { let rehide_ms = {
let config = state.config.read().await; let config = state.config.read().await;
@ -110,7 +80,14 @@ async fn sign_request(req: SignRequest, app_handle: AppHandle, client_pid: u32)
let notification = RequestNotification::new_ssh(request_id, client, key_name.clone()); let notification = RequestNotification::new_ssh(request_id, client, key_name.clone());
app_handle.emit("credential-request", &notification)?; app_handle.emit("credential-request", &notification)?;
let response = chan_recv.await?; 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 { if let Approval::Denied = response.approval {
return Ok(Message::Failure); return Ok(Message::Failure);
} }
@ -137,13 +114,6 @@ async fn sign_request(req: SignRequest, app_handle: AppHandle, client_pid: u32)
} }
fn get_client_pid(stream: &UnixStream) -> std::io::Result<u32> {
let cred = stream.peer_cred()?;
Ok(cred.pid().unwrap() as u32)
}
fn encode_string(buf: &mut Vec<u8>, s: &[u8]) { fn encode_string(buf: &mut Vec<u8>, s: &[u8]) {
let len = s.len() as u32; let len = s.len() as u32;
buf.extend(len.to_be_bytes()); buf.extend(len.to_be_bytes());

View File

@ -1,75 +1,30 @@
use tauri::{AppHandle, Manager};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::oneshot; use tokio::sync::oneshot;
use serde::{Serialize, Deserialize};
use tauri::{AppHandle, Manager};
use crate::errors::*;
use crate::clientinfo::{self, Client}; use crate::clientinfo::{self, Client};
use crate::credentials::{ use crate::errors::*;
AwsBaseCredential,
AwsSessionCredential,
};
use crate::ipc::{Approval, RequestNotification}; use crate::ipc::{Approval, RequestNotification};
use crate::state::AppState;
use crate::shortcuts::{self, ShortcutAction}; use crate::shortcuts::{self, ShortcutAction};
use crate::state::AppState;
#[cfg(windows)] use super::{
mod server_win; CloseWaiter,
#[cfg(windows)] Request,
pub use server_win::Server; Response,
#[cfg(windows)] Stream,
use server_win::Stream; };
#[cfg(unix)]
mod server_unix;
#[cfg(unix)]
pub use server_unix::Server;
#[cfg(unix)]
use server_unix::Stream;
pub mod ssh_agent;
pub use ssh_agent::Agent;
#[derive(Serialize, Deserialize)] pub fn serve(app_handle: AppHandle) -> std::io::Result<()> {
pub enum Request { super::serve("creddy-server", app_handle, handle)
GetAwsCredentials{
base: bool,
},
InvokeShortcut(ShortcutAction),
} }
#[derive(Debug, Serialize, Deserialize)] async fn handle(
pub enum Response { mut stream: Stream,
AwsBase(AwsBaseCredential), app_handle: AppHandle,
AwsSession(AwsSessionCredential), client_pid: u32
Empty, ) -> Result<(), HandlerError> {
}
struct CloseWaiter<'s> {
stream: &'s mut Stream,
}
impl<'s> CloseWaiter<'s> {
async fn wait_for_close(&mut self) -> std::io::Result<()> {
let mut buf = [0u8; 8];
loop {
match self.stream.read(&mut buf).await {
Ok(0) => break Ok(()),
Ok(_) => (),
Err(e) => break Err(e),
}
}
}
}
async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> Result<(), HandlerError>
{
// read from stream until delimiter is reached // read from stream until delimiter is reached
let mut buf: Vec<u8> = Vec::with_capacity(1024); // requests are small, 1KiB is more than enough let mut buf: Vec<u8> = Vec::with_capacity(1024); // requests are small, 1KiB is more than enough
let mut n = 0; let mut n = 0;
@ -78,7 +33,8 @@ async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> R
if let Some(&b'\n') = buf.last() { if let Some(&b'\n') = buf.last() {
break; break;
} }
else if n >= 1024 { // sanity check, no request should ever be within a mile of 1MB
else if n >= (1024 * 1024) {
return Err(HandlerError::RequestTooLarge); return Err(HandlerError::RequestTooLarge);
} }
} }
@ -86,12 +42,14 @@ async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> R
let client = clientinfo::get_client(client_pid, true)?; let client = clientinfo::get_client(client_pid, true)?;
let waiter = CloseWaiter { stream: &mut stream }; let waiter = CloseWaiter { stream: &mut stream };
let req: Request = serde_json::from_slice(&buf)?; let req: Request = serde_json::from_slice(&buf)?;
let res = match req { let res = match req {
Request::GetAwsCredentials{ base } => get_aws_credentials( Request::GetAwsCredentials { name, base } => get_aws_credentials(
base, client, app_handle, waiter name, base, client, app_handle, waiter
).await, ).await,
Request::InvokeShortcut(action) => invoke_shortcut(action).await, Request::InvokeShortcut(action) => invoke_shortcut(action).await,
Request::GetSshSignature(_) => return Err(HandlerError::Denied),
}; };
// doesn't make sense to send the error to the client if the client has already left // doesn't make sense to send the error to the client if the client has already left
@ -112,6 +70,7 @@ async fn invoke_shortcut(action: ShortcutAction) -> Result<Response, HandlerErro
async fn get_aws_credentials( async fn get_aws_credentials(
name: Option<String>,
base: bool, base: bool,
client: Client, client: Client,
app_handle: AppHandle, app_handle: AppHandle,
@ -132,7 +91,9 @@ async fn get_aws_credentials(
// but ? returns immediately, and we want to unregister the request before returning // 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 // so we bundle it all up in an async block and return a Result so we can handle errors
let proceed = async { let proceed = async {
let notification = RequestNotification::new_aws(request_id, client, base); let notification = RequestNotification::new_aws(
request_id, client, name.clone(), base
);
app_handle.emit("credential-request", &notification)?; app_handle.emit("credential-request", &notification)?;
let response = tokio::select! { let response = tokio::select! {
@ -146,11 +107,11 @@ async fn get_aws_credentials(
match response.approval { match response.approval {
Approval::Approved => { Approval::Approved => {
if response.base { if response.base {
let creds = state.get_aws_default().await?; let creds = state.get_aws_base(name).await?;
Ok(Response::AwsBase(creds)) Ok(Response::AwsBase(creds))
} }
else { else {
let creds = state.get_aws_default_session().await?; let creds = state.get_aws_session(name).await?;
Ok(Response::AwsSession(creds.clone())) Ok(Response::AwsSession(creds.clone()))
} }
}, },
@ -163,9 +124,9 @@ async fn get_aws_credentials(
Err(e) => { Err(e) => {
state.unregister_request(request_id).await; state.unregister_request(request_id).await;
Err(e) Err(e)
} },
}; };
lease.release(); lease.release();
result result
} }

170
src-tauri/src/srv/mod.rs Normal file
View File

@ -0,0 +1,170 @@
use std::future::Future;
use tauri::{
AppHandle,
async_runtime as rt,
};
use tokio::io::AsyncReadExt;
use serde::{Serialize, Deserialize};
use ssh_agent_lib::proto::message::SignRequest;
use crate::credentials::{AwsBaseCredential, AwsSessionCredential};
use crate::errors::*;
use crate::shortcuts::ShortcutAction;
pub mod creddy_server;
pub mod agent;
use platform::Stream;
pub use platform::addr;
#[derive(Debug, Serialize, Deserialize)]
pub enum Request {
GetAwsCredentials {
name: Option<String>,
base: bool,
},
GetSshSignature(SignRequest),
InvokeShortcut(ShortcutAction),
}
#[derive(Debug, Serialize, Deserialize)]
pub enum Response {
AwsBase(AwsBaseCredential),
AwsSession(AwsSessionCredential),
Empty,
}
struct CloseWaiter<'s> {
stream: &'s mut Stream,
}
impl<'s> CloseWaiter<'s> {
async fn wait_for_close(&mut self) -> std::io::Result<()> {
let mut buf = [0u8; 8];
loop {
match self.stream.read(&mut buf).await {
Ok(0) => break Ok(()),
Ok(_) => (),
Err(e) => break Err(e),
}
}
}
}
fn serve<H, F>(sock_name: &str, app_handle: AppHandle, handler: H) -> std::io::Result<()>
where H: Copy + Send + Fn(Stream, AppHandle, u32) -> F + 'static,
F: Send + Future<Output = Result<(), HandlerError>>,
{
let (mut listener, addr) = platform::bind(sock_name)?;
rt::spawn(async move {
loop {
let (stream, client_pid) = match platform::accept(&mut listener, &addr).await {
Ok((s, c)) => (s, c),
Err(e) => {
eprintln!("Error accepting request: {e}");
continue;
},
};
let new_handle = app_handle.clone();
rt::spawn(async move {
handler(stream, new_handle, client_pid)
.await
.error_print_prefix("Error responding to request: ");
});
}
});
Ok(())
}
#[cfg(unix)]
mod platform {
use std::io::ErrorKind;
use std::path::PathBuf;
use tokio::net::{UnixListener, UnixStream};
use super::*;
pub type Stream = UnixStream;
pub fn bind(sock_name: &str) -> std::io::Result<(UnixListener, PathBuf)> {
let path = addr(sock_name);
match std::fs::remove_file(&path) {
Ok(_) => (),
Err(e) if e.kind() == ErrorKind::NotFound => (),
Err(e) => return Err(e),
}
let listener = UnixListener::bind(&path)?;
Ok((listener, path))
}
pub async fn accept(listener: &mut UnixListener, _addr: &PathBuf) -> Result<(UnixStream, u32), HandlerError> {
let (stream, _addr) = listener.accept().await?;
let pid = stream.peer_cred()?
.pid()
.ok_or(ClientInfoError::PidNotFound)?
as u32;
Ok((stream, pid))
}
pub fn addr(sock_name: &str) -> PathBuf {
let mut path = dirs::runtime_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"));
path.push(format!("{sock_name}.sock"));
path
}
}
#[cfg(windows)]
mod platform {
use std::os::windows::io::AsRawHandle;
use tokio::net::windows::named_pipe::{
NamedPipeServer,
ServerOptions,
};
use windows::Win32::{
Foundation::HANDLE,
System::Pipes::GetNamedPipeClientProcessId,
};
use super::*;
pub type Stream = NamedPipeServer;
pub fn bind(sock_name: &str) -> std::io::Result<(String, NamedPipeServer)> {
let addr = addr(sock_name);
let listener = ServerOptions::new()
.first_pipe_instance(true)
.create(&addr)?;
Ok((listener, addr))
}
pub async fn accept(listener: &mut NamedPipeServer, addr: &String) -> Result<(NamedPipeServer, u32), HandlerError> {
// connect() just waits for a client to connect, it doesn't return anything
listener.connect().await?;
// unlike Unix sockets, a Windows NamedPipeServer *becomes* the open stream
// once a client connects. If we want to keep listening, we have to construct
// a new server and swap it in.
let new_listener = ServerOptions::new().create(addr)?;
let stream = std::mem::replace(listener, new_listener);
let raw_handle = stream.as_raw_handle();
let mut pid = 0u32;
let handle = HANDLE(raw_handle as _);
unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? };
Ok((stream, pid))
}
pub fn addr(sock_name: &str) -> String {
format!(r"\\.\pipe\{sock_name}")
}
}

View File

@ -270,22 +270,23 @@ impl AppState {
Ok(()) Ok(())
} }
pub async fn get_aws_default(&self) -> Result<AwsBaseCredential, GetCredentialsError> { pub async fn get_aws_base(&self, name: Option<String>) -> Result<AwsBaseCredential, GetCredentialsError> {
let app_session = self.app_session.read().await; let app_session = self.app_session.read().await;
let crypto = app_session.try_get_crypto()?; let crypto = app_session.try_get_crypto()?;
let creds = AwsBaseCredential::load_default(crypto, &self.pool).await?; let creds = match name {
// let record = CredentialRecord::load_default("aws", crypto, &self.pool).await?; Some(n) => AwsBaseCredential::load_by_name(&n, crypto, &self.pool).await?,
// let creds = match record.credential { None => AwsBaseCredential::load_default(crypto, &self.pool).await?,
// Credential::AwsBase(b) => Ok(b), };
// _ => Err(LoadCredentialsError::NoCredentials)
// }?;
Ok(creds) Ok(creds)
} }
pub async fn get_aws_default_session(&self) -> Result<RwLockReadGuard<'_, AwsSessionCredential>, GetCredentialsError> { pub async fn get_aws_session(&self, name: Option<String>) -> Result<RwLockReadGuard<'_, AwsSessionCredential>, GetCredentialsError> {
let app_session = self.app_session.read().await; let app_session = self.app_session.read().await;
let crypto = app_session.try_get_crypto()?; let crypto = app_session.try_get_crypto()?;
let record = CredentialRecord::load_default("aws", crypto, &self.pool).await?; let record = match name {
Some(n) => CredentialRecord::load_by_name(&n, crypto, &self.pool).await?,
None => CredentialRecord::load_default("aws", crypto, &self.pool).await?,
};
let base = match &record.credential { let base = match &record.credential {
Credential::AwsBase(b) => Ok(b), Credential::AwsBase(b) => Ok(b),
_ => Err(LoadCredentialsError::NoCredentials) _ => Err(LoadCredentialsError::NoCredentials)

View File

@ -63,12 +63,12 @@ async fn do_launch(app: &AppHandle, use_base: bool) -> Result<(), LaunchTerminal
// (i.e. lies about unlocking) we could end up here with a locked session // (i.e. lies about unlocking) we could end up here with a locked session
// this will result in an error popup to the user (see main hotkey handler) // this will result in an error popup to the user (see main hotkey handler)
if use_base { if use_base {
let base_creds = state.get_aws_default().await?; let base_creds = state.get_aws_base(None).await?;
cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id); cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key); cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key);
} }
else { else {
let session_creds = state.get_aws_default_session().await?; let session_creds = state.get_aws_session(None).await?;
cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id); cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id);
cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key); cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key);
cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token); cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token);

View File

@ -8,6 +8,11 @@
export {classes as class}; export {classes as class};
let show = false; let show = false;
let input;
export function focus() {
input.focus();
}
</script> </script>
@ -21,6 +26,7 @@
<div class="join w-full has-[:focus]:outline outline-2 outline-offset-2 outline-base-content/20"> <div class="join w-full has-[:focus]:outline outline-2 outline-offset-2 outline-base-content/20">
<input <input
bind:this={input}
type={show ? 'text' : 'password'} type={show ? 'text' : 'password'}
{value} {placeholder} {autofocus} {value} {placeholder} {autofocus}
on:input={e => value = e.target.value} on:input={e => value = e.target.value}

View File

@ -34,6 +34,9 @@
} }
} }
let input;
onMount(() => input.focus());
</script> </script>
@ -52,7 +55,11 @@
<ErrorAlert bind:this="{alert}" /> <ErrorAlert bind:this="{alert}" />
<!-- svelte-ignore a11y-autofocus --> <!-- svelte-ignore a11y-autofocus -->
<PassphraseInput autofocus="true" bind:value={passphrase} placeholder="correct horse battery staple" /> <PassphraseInput
bind:this={input}
bind:value={passphrase}
placeholder="correct horse battery staple"
/>
</label> </label>
<button type="submit" class="btn btn-primary"> <button type="submit" class="btn btn-primary">

View File

@ -44,7 +44,11 @@
<div class="space-y-1 mb-4"> <div class="space-y-1 mb-4">
<h2 class="text-xl font-bold"> <h2 class="text-xl font-bold">
{#if $appState.currentRequest.type === 'Aws'} {#if $appState.currentRequest.type === 'Aws'}
{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials. {#if $appState.currentRequest.name}
{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS access key "{$appState.currentRequest.name}".
{:else}
{appName ? `"${appName}"` : 'An appplication'} would like to access your default AWS access key
{/if}
{:else if $appState.currentRequest.type === 'Ssh'} {:else if $appState.currentRequest.type === 'Ssh'}
{appName ? `"${appName}"` : 'An application'} would like to use your SSH key "{$appState.currentRequest.key_name}". {appName ? `"${appName}"` : 'An application'} would like to use your SSH key "{$appState.currentRequest.key_name}".
{/if} {/if}