From 0124f77f7b4c8446ce68d8d3006ccad7156dbd7a Mon Sep 17 00:00:00 2001 From: Joseph Montanaro Date: Wed, 3 Jul 2024 06:33:58 -0400 Subject: [PATCH] initial working implementation of ssh agent --- src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 2 + .../20240617142724_credential_split.sql | 3 +- src-tauri/src/_credentials.rs | 350 ------------------ src-tauri/src/app.rs | 3 +- src-tauri/src/bin/agent.rs | 7 - src-tauri/src/clientinfo.rs | 22 +- src-tauri/src/credentials/mod.rs | 2 + src-tauri/src/credentials/ssh.rs | 33 ++ src-tauri/src/errors.rs | 4 + src-tauri/src/server/_ssh_agent.rs | 77 ++++ src-tauri/src/server/mod.rs | 3 +- src-tauri/src/server/ssh_agent.rs | 193 +++++++--- src-tauri/src/state.rs | 18 + src/views/approve/CollectResponse.svelte | 44 ++- 15 files changed, 318 insertions(+), 445 deletions(-) delete mode 100644 src-tauri/src/_credentials.rs delete mode 100644 src-tauri/src/bin/agent.rs create mode 100644 src-tauri/src/server/_ssh_agent.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b0838e8..368dc7a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1208,6 +1208,7 @@ dependencies = [ "chacha20poly1305", "clap", "dirs 5.0.1", + "futures", "is-terminal", "once_cell", "rfd 0.13.0", @@ -1231,6 +1232,7 @@ dependencies = [ "time", "tokio", "tokio-stream", + "tokio-util", "which", "windows 0.51.1", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5b30d88..da2f1f2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -56,6 +56,8 @@ ssh-key = { version = "0.6.6", features = ["rsa", "ed25519", "encryption"] } signature = "2.2.0" tokio-stream = "0.1.15" sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "uuid"] } +tokio-util = { version = "0.7.11", features = ["codec"] } +futures = "0.3.30" [features] # by default Tauri runs in production mode diff --git a/src-tauri/migrations/20240617142724_credential_split.sql b/src-tauri/migrations/20240617142724_credential_split.sql index 8e55c9d..c87abc7 100644 --- a/src-tauri/migrations/20240617142724_credential_split.sql +++ b/src-tauri/migrations/20240617142724_credential_split.sql @@ -75,5 +75,6 @@ CREATE TABLE ssh_credentials ( comment TEXT NOT NULL, public_key BLOB NOT NULL, private_key_enc BLOB NOT NULL, - nonce BLOB NOT NULL + nonce BLOB NOT NULL, + FOREIGN KEY(id) REFERENCES credentials(id) ON DELETE CASCADE ); diff --git a/src-tauri/src/_credentials.rs b/src-tauri/src/_credentials.rs deleted file mode 100644 index 3608318..0000000 --- a/src-tauri/src/_credentials.rs +++ /dev/null @@ -1,350 +0,0 @@ -use std::fmt::{self, Formatter}; -use std::time::{SystemTime, UNIX_EPOCH}; - - use aws_smithy_types::date_time::{DateTime, Format}; -use argon2::{ - Argon2, - Algorithm, - Version, - ParamsBuilder, - password_hash::rand_core::{RngCore, OsRng}, -}; -use chacha20poly1305::{ - XChaCha20Poly1305, - XNonce, - aead::{ - Aead, - AeadCore, - KeyInit, - Error as AeadError, - generic_array::GenericArray, - }, -}; -use serde::{ - Serialize, - Deserialize, - Serializer, - Deserializer, -}; -use serde::de::{self, Visitor}; -use sqlx::SqlitePool; - - -use crate::errors::*; - - -#[derive(Clone, Debug)] -pub enum Session { - Unlocked{ - base: BaseCredentials, - session: SessionCredentials, - }, - Locked(LockedCredentials), - Empty, -} - -impl Session { - pub async fn load(pool: &SqlitePool) -> Result { - let res = sqlx::query!("SELECT * FROM credentials ORDER BY created_at desc") - .fetch_optional(pool) - .await?; - let row = match res { - Some(r) => r, - None => {return Ok(Session::Empty);} - }; - - let salt: [u8; 32] = row.salt - .try_into() - .map_err(|_e| SetupError::InvalidRecord)?; - let nonce = XNonce::from_exact_iter(row.nonce.into_iter()) - .ok_or(SetupError::InvalidRecord)?; - - let creds = LockedCredentials { - access_key_id: row.access_key_id, - secret_key_enc: row.secret_key_enc, - salt, - nonce, - }; - Ok(Session::Locked(creds)) - } - - pub async fn renew_if_expired(&mut self) -> Result { - match self { - Session::Unlocked{ref base, ref mut session} => { - if !session.is_expired() { - return Ok(false); - } - *session = SessionCredentials::from_base(base).await?; - Ok(true) - }, - Session::Locked(_) => Err(GetSessionError::CredentialsLocked), - Session::Empty => Err(GetSessionError::CredentialsEmpty), - } - } - - pub fn try_get( - &self - ) -> Result<(&BaseCredentials, &SessionCredentials), GetCredentialsError> { - match self { - Self::Empty => Err(GetCredentialsError::Empty), - Self::Locked(_) => Err(GetCredentialsError::Locked), - Self::Unlocked{ ref base, ref session } => Ok((base, session)) - } - } -} - - -#[derive(Clone, Debug)] -pub struct LockedCredentials { - pub access_key_id: String, - pub secret_key_enc: Vec, - pub salt: [u8; 32], - pub nonce: XNonce, -} - -impl LockedCredentials { - pub async fn save(&self, pool: &SqlitePool) -> Result<(), sqlx::Error> { - sqlx::query( - "INSERT INTO credentials (access_key_id, secret_key_enc, salt, nonce, created_at) - VALUES (?, ?, ?, ?, strftime('%s'))" - ) - .bind(&self.access_key_id) - .bind(&self.secret_key_enc) - .bind(&self.salt[..]) - .bind(&self.nonce[..]) - .execute(pool) - .await?; - - Ok(()) - } - - pub fn decrypt(&self, passphrase: &str) -> Result { - let crypto = Crypto::new(passphrase, &self.salt) - .map_err(|e| CryptoError::Argon2(e))?; - let decrypted = crypto.decrypt(&self.nonce, &self.secret_key_enc) - .map_err(|e| CryptoError::Aead(e))?; - let secret_access_key = String::from_utf8(decrypted) - .map_err(|_| UnlockError::InvalidUtf8)?; - - let creds = BaseCredentials::new( - self.access_key_id.clone(), - secret_access_key, - ); - Ok(creds) - } -} - - -fn default_credentials_version() -> usize { 1 } - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "PascalCase")] -pub struct BaseCredentials { - #[serde(default = "default_credentials_version")] - pub version: usize, - pub access_key_id: String, - pub secret_access_key: String, -} - -impl BaseCredentials { - pub fn new(access_key_id: String, secret_access_key: String) -> Self { - Self {version: 1, access_key_id, secret_access_key} - } - - pub fn encrypt(&self, passphrase: &str) -> Result { - let salt = Crypto::salt(); - let crypto = Crypto::new(passphrase, &salt)?; - let (nonce, secret_key_enc) = crypto.encrypt(self.secret_access_key.as_bytes())?; - - let locked = LockedCredentials { - access_key_id: self.access_key_id.clone(), - secret_key_enc, - salt, - nonce, - }; - Ok(locked) - } -} - - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "PascalCase")] -pub struct SessionCredentials { - #[serde(default = "default_credentials_version")] - pub version: usize, - pub access_key_id: String, - pub secret_access_key: String, - pub session_token: String, - #[serde(serialize_with = "serialize_expiration")] - #[serde(deserialize_with = "deserialize_expiration")] - pub expiration: DateTime, -} - -impl SessionCredentials { - pub async fn from_base(base: &BaseCredentials) -> Result { - let req_creds = aws_sdk_sts::Credentials::new( - &base.access_key_id, - &base.secret_access_key, - None, // token - None, //expiration - "Creddy", // "provider name" apparently - ); - let config = aws_config::from_env() - .credentials_provider(req_creds) - .load() - .await; - - let client = aws_sdk_sts::Client::new(&config); - let resp = client.get_session_token() - .duration_seconds(43_200) - .send() - .await?; - - let aws_session = resp.credentials().ok_or(GetSessionError::EmptyResponse)?; - - let access_key_id = aws_session.access_key_id() - .ok_or(GetSessionError::EmptyResponse)? - .to_string(); - let secret_access_key = aws_session.secret_access_key() - .ok_or(GetSessionError::EmptyResponse)? - .to_string(); - let session_token = aws_session.session_token() - .ok_or(GetSessionError::EmptyResponse)? - .to_string(); - let expiration = aws_session.expiration() - .ok_or(GetSessionError::EmptyResponse)? - .clone(); - - let session_creds = SessionCredentials { - version: 1, - access_key_id, - secret_access_key, - session_token, - expiration, - }; - - #[cfg(debug_assertions)] - println!("Got new session:\n{}", serde_json::to_string(&session_creds).unwrap()); - - Ok(session_creds) - } - - pub fn is_expired(&self) -> bool { - let current_ts = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() // doesn't panic because UNIX_EPOCH won't be later than now() - .as_secs(); - - let expire_ts = self.expiration.secs(); - let remaining = expire_ts - (current_ts as i64); - remaining < 60 - } -} - - -#[derive(Debug, Serialize, Deserialize)] -pub enum Credentials { - Base(BaseCredentials), - Session(SessionCredentials), -} - - -fn serialize_expiration(exp: &DateTime, serializer: S) -> Result -where S: Serializer -{ - // this only fails if the d/t is out of range, which it can't be for this format - let time_str = exp.fmt(Format::DateTime).unwrap(); - serializer.serialize_str(&time_str) -} - - -struct DateTimeVisitor; - -impl<'de> Visitor<'de> for DateTimeVisitor { - type Value = DateTime; - - fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { - write!(formatter, "an RFC 3339 UTC string, e.g. \"2014-01-05T10:17:34Z\"") - } - - fn visit_str(self, v: &str) -> Result { - DateTime::from_str(v, Format::DateTime) - .map_err(|_| E::custom(format!("Invalid date/time: {v}"))) - } -} - - -fn deserialize_expiration<'de, D>(deserializer: D) -> Result -where D: Deserializer<'de> -{ - deserializer.deserialize_str(DateTimeVisitor) -} - - -struct Crypto { - cipher: XChaCha20Poly1305, -} - -impl Crypto { - /// Argon2 params rationale: - /// - /// m_cost is measured in KiB, so 128 * 1024 gives us 128MiB. - /// This should roughly double the memory usage of the application - /// while deriving the key. - /// - /// p_cost is irrelevant since (at present) there isn't any parallelism - /// implemented, so we leave it at 1. - /// - /// With the above m_cost, t_cost = 8 results in about 800ms to derive - /// a key on my (somewhat older) CPU. This is probably overkill, but - /// given that it should only have to happen ~once a day for most - /// usage, it should be acceptable. - #[cfg(not(debug_assertions))] - const MEM_COST: u32 = 128 * 1024; - #[cfg(not(debug_assertions))] - const TIME_COST: u32 = 8; - - /// But since this takes a million years without optimizations, - /// we turn it way down in debug builds. - #[cfg(debug_assertions)] - const MEM_COST: u32 = 48 * 1024; - #[cfg(debug_assertions)] - const TIME_COST: u32 = 1; - - - fn new(passphrase: &str, salt: &[u8]) -> argon2::Result { - let params = ParamsBuilder::new() - .m_cost(Self::MEM_COST) - .p_cost(1) - .t_cost(Self::TIME_COST) - .build() - .unwrap(); // only errors if the given params are invalid - - let hasher = Argon2::new( - Algorithm::Argon2id, - Version::V0x13, - params, - ); - - let mut key = [0; 32]; - hasher.hash_password_into(passphrase.as_bytes(), &salt, &mut key)?; - let cipher = XChaCha20Poly1305::new(GenericArray::from_slice(&key)); - Ok(Crypto { cipher }) - } - - fn salt() -> [u8; 32] { - let mut salt = [0; 32]; - OsRng.fill_bytes(&mut salt); - salt - } - - fn encrypt(&self, data: &[u8]) -> Result<(XNonce, Vec), AeadError> { - let nonce = XChaCha20Poly1305::generate_nonce(&mut OsRng); - let ciphertext = self.cipher.encrypt(&nonce, data)?; - Ok((nonce, ciphertext)) - } - - fn decrypt(&self, nonce: &XNonce, data: &[u8]) -> Result, AeadError> { - self.cipher.decrypt(nonce, data) - } -} diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index c9d1205..d826c3a 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -25,7 +25,7 @@ use crate::{ config::{self, AppConfig}, credentials::AppSession, ipc, - server::Server, + server::{Server, Agent}, errors::*, shortcuts, state::AppState, @@ -110,6 +110,7 @@ async fn setup(app: &mut App) -> Result<(), Box> { let app_session = AppSession::load(&pool).await?; Server::start(app.handle().clone())?; + Agent::start(app.handle().clone())?; config::set_auto_launch(conf.start_on_login)?; if let Err(_e) = config::set_auto_launch(conf.start_on_login) { diff --git a/src-tauri/src/bin/agent.rs b/src-tauri/src/bin/agent.rs deleted file mode 100644 index 45f6436..0000000 --- a/src-tauri/src/bin/agent.rs +++ /dev/null @@ -1,7 +0,0 @@ -use creddy::server::ssh_agent; - - -#[tokio::main] -async fn main() { - ssh_agent::run().await; -} diff --git a/src-tauri/src/clientinfo.rs b/src-tauri/src/clientinfo.rs index bd8cf91..2a93c03 100644 --- a/src-tauri/src/clientinfo.rs +++ b/src-tauri/src/clientinfo.rs @@ -1,6 +1,6 @@ use std::path::{Path, PathBuf}; -use sysinfo::{System, SystemExt, Pid, PidExt, ProcessExt}; +use sysinfo::{System, SystemExt, Pid, PidExt, Process, ProcessExt}; use serde::{Serialize, Deserialize}; use crate::errors::*; @@ -13,23 +13,25 @@ pub struct Client { } -pub fn get_process_parent_info(pid: u32) -> Result { +pub fn get_client(pid: u32, parent: bool) -> Result { let sys_pid = Pid::from_u32(pid); let mut sys = System::new(); sys.refresh_process(sys_pid); - let proc = sys.process(sys_pid) + let mut proc = sys.process(sys_pid) .ok_or(ClientInfoError::ProcessNotFound)?; - let parent_pid_sys = proc.parent() - .ok_or(ClientInfoError::ParentPidNotFound)?; - sys.refresh_process(parent_pid_sys); - let parent = sys.process(parent_pid_sys) - .ok_or(ClientInfoError::ParentProcessNotFound)?; + if parent { + let parent_pid_sys = proc.parent() + .ok_or(ClientInfoError::ParentPidNotFound)?; + sys.refresh_process(parent_pid_sys); + proc = sys.process(parent_pid_sys) + .ok_or(ClientInfoError::ParentProcessNotFound)?; + } - let exe = match parent.exe() { + let exe = match proc.exe() { p if p == Path::new("") => None, p => Some(PathBuf::from(p)), }; - Ok(Client { pid: parent_pid_sys.as_u32(), exe }) + Ok(Client { pid: proc.pid().as_u32(), exe }) } diff --git a/src-tauri/src/credentials/mod.rs b/src-tauri/src/credentials/mod.rs index 3954dd5..f8f3dc9 100644 --- a/src-tauri/src/credentials/mod.rs +++ b/src-tauri/src/credentials/mod.rs @@ -1,10 +1,12 @@ use serde::{Serialize, Deserialize}; use sqlx::{ + Encode, FromRow, Sqlite, SqlitePool, sqlite::SqliteRow, Transaction, + Type, types::Uuid, }; use tokio_stream::StreamExt; diff --git a/src-tauri/src/credentials/ssh.rs b/src-tauri/src/credentials/ssh.rs index 8c0c27b..6fdb956 100644 --- a/src-tauri/src/credentials/ssh.rs +++ b/src-tauri/src/credentials/ssh.rs @@ -21,12 +21,14 @@ use sqlx::{ Transaction, types::Uuid, }; +use ssh_agent_lib::proto::message::Identity; use ssh_key::{ Algorithm, LineEnding, private::PrivateKey, public::PublicKey, }; +use tokio_stream::StreamExt; use crate::errors::*; use super::{ @@ -73,6 +75,37 @@ impl SshKey { private_key: privkey, }) } + + pub async fn name_from_pubkey(pubkey: &[u8], pool: &SqlitePool) -> Result { + let row = sqlx::query!( + "SELECT c.name + FROM credentials c + JOIN ssh_credentials s + ON s.id = c.id + WHERE s.public_key = ?", + pubkey + ).fetch_optional(pool) + .await? + .ok_or(LoadCredentialsError::NoCredentials)?; + + Ok(row.name) + } + + pub async fn list_identities(pool: &SqlitePool) -> Result, LoadCredentialsError> { + let mut rows = sqlx::query!( + "SELECT public_key, comment FROM ssh_credentials" + ).fetch(pool); + + let mut identities = Vec::new(); + while let Some(row) = rows.try_next().await? { + identities.push(Identity { + pubkey_blob: row.public_key, + comment: row.comment, + }); + } + + Ok(identities) + } } diff --git a/src-tauri/src/errors.rs b/src-tauri/src/errors.rs index c369f75..602def2 100644 --- a/src-tauri/src/errors.rs +++ b/src-tauri/src/errors.rs @@ -191,6 +191,10 @@ pub enum HandlerError { NoMainWindow, #[error("Request was denied")] Denied, + #[error(transparent)] + SshAgent(#[from] ssh_agent_lib::error::AgentError), + #[error(transparent)] + SshKey(#[from] ssh_key::Error), } diff --git a/src-tauri/src/server/_ssh_agent.rs b/src-tauri/src/server/_ssh_agent.rs new file mode 100644 index 0000000..f1d21ac --- /dev/null +++ b/src-tauri/src/server/_ssh_agent.rs @@ -0,0 +1,77 @@ +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> { + 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, 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(); +} diff --git a/src-tauri/src/server/mod.rs b/src-tauri/src/server/mod.rs index 73dc27a..9c196d5 100644 --- a/src-tauri/src/server/mod.rs +++ b/src-tauri/src/server/mod.rs @@ -30,6 +30,7 @@ pub use server_unix::Server; use server_unix::Stream; pub mod ssh_agent; +pub use ssh_agent::Agent; #[derive(Serialize, Deserialize)] @@ -82,7 +83,7 @@ async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> R } } - let client = clientinfo::get_process_parent_info(client_pid)?; + let client = clientinfo::get_client(client_pid, true)?; let waiter = CloseWaiter { stream: &mut stream }; let req: Request = serde_json::from_slice(&buf)?; diff --git a/src-tauri/src/server/ssh_agent.rs b/src-tauri/src/server/ssh_agent.rs index f1d21ac..5df1e81 100644 --- a/src-tauri/src/server/ssh_agent.rs +++ b/src-tauri/src/server/ssh_agent.rs @@ -1,64 +1,147 @@ +use std::io::ErrorKind; + +use futures::SinkExt; 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; +use ssh_agent_lib::agent::MessageCodec; +use ssh_agent_lib::proto::message::{ + Message, + Identity, + SignRequest, +}; +use tokio::net::{UnixListener, UnixStream}; +use tauri::{ + AppHandle, + Manager, + async_runtime as rt, +}; +use tokio_util::codec::Framed; +use tokio_stream::StreamExt; +use tokio::sync::oneshot; + +use crate::clientinfo; +use crate::credentials::{Credential, SshKey}; +use crate::errors::*; +use crate::ipc::{Approval, RequestNotification}; +use crate::state::AppState; -struct SshAgent; +pub struct Agent { + listener: UnixListener, + app_handle: AppHandle, +} -impl std::default::Default for SshAgent { - fn default() -> Self { - SshAgent {} +impl Agent { + pub fn start(app_handle: AppHandle) -> std::io::Result<()> { + 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(()) } } -#[ssh_agent_lib::async_trait] -impl Session for SshAgent { - async fn handle(&mut self, message: Message) -> Result> { - 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(); +async fn handle_framed( + mut adapter: Framed, + app_handle: AppHandle, + client_pid: u32, +) -> Result<(), HandlerError> { + while let Some(message) = adapter.try_next().await? { + let resp = match message { + Message::RequestIdentities => list_identities(app_handle.clone()).await?, + Message::SignRequest(req) => sign_request(req, app_handle.clone(), client_pid).await?, + _ => Message::Failure, + }; - - - 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), - } + adapter.send(resp).await?; } + + Ok(()) +} + + +async fn list_identities(app_handle: AppHandle) -> Result { + let state = app_handle.state::(); + let identities: Vec = state.list_ssh_identities().await?; + Ok(Message::IdentitiesAnswer(identities)) +} + + +async fn sign_request(req: SignRequest, app_handle: AppHandle, client_pid: u32) -> 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 (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 = chan_recv.await?; + if let Approval::Denied = response.approval { + return Ok(Message::Failure); + } + + let key = state.sshkey_by_name(&key_name).await?; + let sig = Signer::sign(&key.private_key, &req.data); + let key_type = key.algorithm.as_str().as_bytes(); + + let payload_len = key_type.len() + sig.as_bytes().len() + 8; + let mut payload = Vec::with_capacity(payload_len); + encode_string(&mut payload, key.algorithm.as_str().as_bytes()); + encode_string(&mut payload, sig.as_bytes()); + + Ok(Message::SignResponse(payload)) + }; + + let res = proceed.await; + if let Err(e) = &res { + state.unregister_request(request_id).await; + } + + lease.release(); + res +} + + + +fn get_client_pid(stream: &UnixStream) -> std::io::Result { + let cred = stream.peer_cred()?; + Ok(cred.pid().unwrap() as u32) } @@ -67,11 +150,3 @@ fn encode_string(buf: &mut Vec, s: &[u8]) { 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(); -} diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 9b7a43d..f126b5a 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -7,6 +7,7 @@ use tokio::{ sync::{RwLock, RwLockReadGuard}, sync::oneshot::{self, Sender}, }; +use ssh_agent_lib::proto::message::Identity; use sqlx::SqlitePool; use sqlx::types::Uuid; use tauri::{ @@ -18,6 +19,7 @@ use crate::app; use crate::credentials::{ AppSession, AwsSessionCredential, + SshKey, }; use crate::{config, config::AppConfig}; use crate::credentials::{ @@ -165,6 +167,10 @@ impl AppState { Ok(list) } + pub async fn list_ssh_identities(&self) -> Result, GetCredentialsError> { + Ok(SshKey::list_identities(&self.pool).await?) + } + pub async fn set_passphrase(&self, passphrase: &str) -> Result<(), SaveCredentialsError> { let mut cur_session = self.app_session.write().await; if let AppSession::Locked {..} = *cur_session { @@ -302,6 +308,18 @@ impl AppState { Ok(s) } + pub async fn ssh_name_from_pubkey(&self, pubkey: &[u8]) -> Result { + let k = SshKey::name_from_pubkey(pubkey, &self.pool).await?; + Ok(k) + } + + pub async fn sshkey_by_name(&self, name: &str) -> Result { + let app_session = self.app_session.read().await; + let crypto = app_session.try_get_crypto()?; + let k = SshKey::load_by_name(name, crypto, &self.pool).await?; + Ok(k) + } + 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 485a602..9f9584e 100644 --- a/src/views/approve/CollectResponse.svelte +++ b/src/views/approve/CollectResponse.svelte @@ -42,7 +42,13 @@ {/if}
-

{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.

+

+ {#if $appState.currentRequest.type === 'Aws'} + {appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials. + {:else if $appState.currentRequest.type === 'Ssh'} + {appName ? `"${appName}"` : 'An application'} would like to use your SSH key "{$appState.currentRequest.key_name}". + {/if} +

Path:
@@ -56,7 +62,11 @@ {#if !$appState.currentRequest?.base}

- Approve with session credentials + {#if $appState.currentRequest.type === 'Aws'} + Approve with session credentials + {:else} + Approve + {/if}

setResponse('Approved', false)} hotkey="Enter" shift={true}> - + {#if $appState.currentRequest.type === 'Aws'} +

+ + {#if $appState.currentRequest?.base} + Approve + {:else} + Approve with base credentials + {/if} + +

+ setResponse('Approved', true)} hotkey="Enter" shift={true} ctrl={true}> + + + {/if}

Deny