initial working implementation of ssh agent
This commit is contained in:
parent
6711ce2c43
commit
0124f77f7b
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -1208,6 +1208,7 @@ dependencies = [
|
|||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
"clap",
|
"clap",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
"futures",
|
||||||
"is-terminal",
|
"is-terminal",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rfd 0.13.0",
|
"rfd 0.13.0",
|
||||||
@ -1231,6 +1232,7 @@ dependencies = [
|
|||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
"tokio-util",
|
||||||
"which",
|
"which",
|
||||||
"windows 0.51.1",
|
"windows 0.51.1",
|
||||||
]
|
]
|
||||||
|
@ -56,6 +56,8 @@ ssh-key = { version = "0.6.6", features = ["rsa", "ed25519", "encryption"] }
|
|||||||
signature = "2.2.0"
|
signature = "2.2.0"
|
||||||
tokio-stream = "0.1.15"
|
tokio-stream = "0.1.15"
|
||||||
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "uuid"] }
|
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "uuid"] }
|
||||||
|
tokio-util = { version = "0.7.11", features = ["codec"] }
|
||||||
|
futures = "0.3.30"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
|
@ -75,5 +75,6 @@ CREATE TABLE ssh_credentials (
|
|||||||
comment TEXT NOT NULL,
|
comment TEXT NOT NULL,
|
||||||
public_key BLOB NOT NULL,
|
public_key BLOB NOT NULL,
|
||||||
private_key_enc 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
|
||||||
);
|
);
|
||||||
|
@ -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<Self, SetupError> {
|
|
||||||
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<bool, GetSessionError> {
|
|
||||||
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<u8>,
|
|
||||||
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<BaseCredentials, UnlockError> {
|
|
||||||
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<LockedCredentials, CryptoError> {
|
|
||||||
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<Self, GetSessionError> {
|
|
||||||
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<S>(exp: &DateTime, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
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<E: de::Error>(self, v: &str) -> Result<DateTime, E> {
|
|
||||||
DateTime::from_str(v, Format::DateTime)
|
|
||||||
.map_err(|_| E::custom(format!("Invalid date/time: {v}")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn deserialize_expiration<'de, D>(deserializer: D) -> Result<DateTime, D::Error>
|
|
||||||
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<Crypto> {
|
|
||||||
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<u8>), 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<Vec<u8>, AeadError> {
|
|
||||||
self.cipher.decrypt(nonce, data)
|
|
||||||
}
|
|
||||||
}
|
|
@ -25,7 +25,7 @@ use crate::{
|
|||||||
config::{self, AppConfig},
|
config::{self, AppConfig},
|
||||||
credentials::AppSession,
|
credentials::AppSession,
|
||||||
ipc,
|
ipc,
|
||||||
server::Server,
|
server::{Server, Agent},
|
||||||
errors::*,
|
errors::*,
|
||||||
shortcuts,
|
shortcuts,
|
||||||
state::AppState,
|
state::AppState,
|
||||||
@ -110,6 +110,7 @@ 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())?;
|
Server::start(app.handle().clone())?;
|
||||||
|
Agent::start(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) {
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
use creddy::server::ssh_agent;
|
|
||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
ssh_agent::run().await;
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
use std::path::{Path, PathBuf};
|
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 serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
@ -13,23 +13,25 @@ pub struct Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn get_process_parent_info(pid: u32) -> Result<Client, ClientInfoError> {
|
pub fn get_client(pid: u32, parent: bool) -> Result<Client, ClientInfoError> {
|
||||||
let sys_pid = Pid::from_u32(pid);
|
let sys_pid = Pid::from_u32(pid);
|
||||||
let mut sys = System::new();
|
let mut sys = System::new();
|
||||||
sys.refresh_process(sys_pid);
|
sys.refresh_process(sys_pid);
|
||||||
let proc = sys.process(sys_pid)
|
let mut proc = sys.process(sys_pid)
|
||||||
.ok_or(ClientInfoError::ProcessNotFound)?;
|
.ok_or(ClientInfoError::ProcessNotFound)?;
|
||||||
|
|
||||||
|
if parent {
|
||||||
let parent_pid_sys = proc.parent()
|
let parent_pid_sys = proc.parent()
|
||||||
.ok_or(ClientInfoError::ParentPidNotFound)?;
|
.ok_or(ClientInfoError::ParentPidNotFound)?;
|
||||||
sys.refresh_process(parent_pid_sys);
|
sys.refresh_process(parent_pid_sys);
|
||||||
let parent = sys.process(parent_pid_sys)
|
proc = sys.process(parent_pid_sys)
|
||||||
.ok_or(ClientInfoError::ParentProcessNotFound)?;
|
.ok_or(ClientInfoError::ParentProcessNotFound)?;
|
||||||
|
}
|
||||||
|
|
||||||
let exe = match parent.exe() {
|
let exe = match proc.exe() {
|
||||||
p if p == Path::new("") => None,
|
p if p == Path::new("") => None,
|
||||||
p => Some(PathBuf::from(p)),
|
p => Some(PathBuf::from(p)),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Client { pid: parent_pid_sys.as_u32(), exe })
|
Ok(Client { pid: proc.pid().as_u32(), exe })
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
|
Encode,
|
||||||
FromRow,
|
FromRow,
|
||||||
Sqlite,
|
Sqlite,
|
||||||
SqlitePool,
|
SqlitePool,
|
||||||
sqlite::SqliteRow,
|
sqlite::SqliteRow,
|
||||||
Transaction,
|
Transaction,
|
||||||
|
Type,
|
||||||
types::Uuid,
|
types::Uuid,
|
||||||
};
|
};
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
|
@ -21,12 +21,14 @@ use sqlx::{
|
|||||||
Transaction,
|
Transaction,
|
||||||
types::Uuid,
|
types::Uuid,
|
||||||
};
|
};
|
||||||
|
use ssh_agent_lib::proto::message::Identity;
|
||||||
use ssh_key::{
|
use ssh_key::{
|
||||||
Algorithm,
|
Algorithm,
|
||||||
LineEnding,
|
LineEnding,
|
||||||
private::PrivateKey,
|
private::PrivateKey,
|
||||||
public::PublicKey,
|
public::PublicKey,
|
||||||
};
|
};
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
use crate::errors::*;
|
use crate::errors::*;
|
||||||
use super::{
|
use super::{
|
||||||
@ -73,6 +75,37 @@ impl SshKey {
|
|||||||
private_key: privkey,
|
private_key: privkey,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn name_from_pubkey(pubkey: &[u8], pool: &SqlitePool) -> Result<String, LoadCredentialsError> {
|
||||||
|
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<Vec<Identity>, 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -191,6 +191,10 @@ pub enum HandlerError {
|
|||||||
NoMainWindow,
|
NoMainWindow,
|
||||||
#[error("Request was denied")]
|
#[error("Request was denied")]
|
||||||
Denied,
|
Denied,
|
||||||
|
#[error(transparent)]
|
||||||
|
SshAgent(#[from] ssh_agent_lib::error::AgentError),
|
||||||
|
#[error(transparent)]
|
||||||
|
SshKey(#[from] ssh_key::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
77
src-tauri/src/server/_ssh_agent.rs
Normal file
77
src-tauri/src/server/_ssh_agent.rs
Normal file
@ -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<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();
|
||||||
|
}
|
@ -30,6 +30,7 @@ pub use server_unix::Server;
|
|||||||
use server_unix::Stream;
|
use server_unix::Stream;
|
||||||
|
|
||||||
pub mod ssh_agent;
|
pub mod ssh_agent;
|
||||||
|
pub use ssh_agent::Agent;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[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 waiter = CloseWaiter { stream: &mut stream };
|
||||||
|
|
||||||
let req: Request = serde_json::from_slice(&buf)?;
|
let req: Request = serde_json::from_slice(&buf)?;
|
||||||
|
@ -1,64 +1,147 @@
|
|||||||
|
use std::io::ErrorKind;
|
||||||
|
|
||||||
|
use futures::SinkExt;
|
||||||
use signature::Signer;
|
use signature::Signer;
|
||||||
use ssh_agent_lib::agent::{Agent, Session};
|
use ssh_agent_lib::agent::MessageCodec;
|
||||||
use ssh_agent_lib::proto::message::Message;
|
use ssh_agent_lib::proto::message::{
|
||||||
use ssh_key::public::PublicKey;
|
Message,
|
||||||
use ssh_key::private::PrivateKey;
|
Identity,
|
||||||
use tokio::net::UnixListener;
|
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 {
|
impl Agent {
|
||||||
fn default() -> Self {
|
pub fn start(app_handle: AppHandle) -> std::io::Result<()> {
|
||||||
SshAgent {}
|
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_framed(
|
||||||
async fn handle(&mut self, message: Message) -> Result<Message, Box<dyn std::error::Error>> {
|
mut adapter: Framed<UnixStream, MessageCodec>,
|
||||||
println!("Received message");
|
app_handle: AppHandle,
|
||||||
match message {
|
client_pid: u32,
|
||||||
Message::RequestIdentities => {
|
) -> Result<(), HandlerError> {
|
||||||
let p = std::path::PathBuf::from("/home/joe/.ssh/id_ed25519.pub");
|
while let Some(message) = adapter.try_next().await? {
|
||||||
let pubkey = PublicKey::read_openssh_file(&p).unwrap();
|
let resp = match message {
|
||||||
let id = ssh_agent_lib::proto::message::Identity {
|
Message::RequestIdentities => list_identities(app_handle.clone()).await?,
|
||||||
pubkey_blob: pubkey.to_bytes().unwrap(),
|
Message::SignRequest(req) => sign_request(req, app_handle.clone(), client_pid).await?,
|
||||||
comment: pubkey.comment().to_owned(),
|
_ => Message::Failure,
|
||||||
};
|
};
|
||||||
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");
|
adapter.send(resp).await?;
|
||||||
let passphrase = std::env::var("PRIVKEY_PASSPHRASE").unwrap();
|
}
|
||||||
let privkey = PrivateKey::read_openssh_file(&p)
|
|
||||||
.unwrap()
|
Ok(())
|
||||||
.decrypt(passphrase.as_bytes())
|
}
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
|
|
||||||
|
async fn list_identities(app_handle: AppHandle) -> Result<Message, HandlerError> {
|
||||||
|
let state = app_handle.state::<AppState>();
|
||||||
|
let identities: Vec<Identity> = state.list_ssh_identities().await?;
|
||||||
|
Ok(Message::IdentitiesAnswer(identities))
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
async fn sign_request(req: SignRequest, app_handle: AppHandle, client_pid: u32) -> Result<Message, HandlerError> {
|
||||||
encode_string(&mut payload, "ssh-ed25519".as_bytes());
|
let state = app_handle.state::<AppState>();
|
||||||
|
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());
|
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::SignResponse(payload))
|
||||||
},
|
};
|
||||||
_ => Ok(Message::Failure),
|
|
||||||
}
|
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<u32> {
|
||||||
|
let cred = stream.peer_cred()?;
|
||||||
|
Ok(cred.pid().unwrap() as u32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -67,11 +150,3 @@ fn encode_string(buf: &mut Vec<u8>, s: &[u8]) {
|
|||||||
buf.extend(len.to_be_bytes());
|
buf.extend(len.to_be_bytes());
|
||||||
buf.extend(s);
|
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();
|
|
||||||
}
|
|
||||||
|
@ -7,6 +7,7 @@ use tokio::{
|
|||||||
sync::{RwLock, RwLockReadGuard},
|
sync::{RwLock, RwLockReadGuard},
|
||||||
sync::oneshot::{self, Sender},
|
sync::oneshot::{self, Sender},
|
||||||
};
|
};
|
||||||
|
use ssh_agent_lib::proto::message::Identity;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use sqlx::types::Uuid;
|
use sqlx::types::Uuid;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
@ -18,6 +19,7 @@ use crate::app;
|
|||||||
use crate::credentials::{
|
use crate::credentials::{
|
||||||
AppSession,
|
AppSession,
|
||||||
AwsSessionCredential,
|
AwsSessionCredential,
|
||||||
|
SshKey,
|
||||||
};
|
};
|
||||||
use crate::{config, config::AppConfig};
|
use crate::{config, config::AppConfig};
|
||||||
use crate::credentials::{
|
use crate::credentials::{
|
||||||
@ -165,6 +167,10 @@ impl AppState {
|
|||||||
Ok(list)
|
Ok(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_ssh_identities(&self) -> Result<Vec<Identity>, GetCredentialsError> {
|
||||||
|
Ok(SshKey::list_identities(&self.pool).await?)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn set_passphrase(&self, passphrase: &str) -> Result<(), SaveCredentialsError> {
|
pub async fn set_passphrase(&self, passphrase: &str) -> Result<(), SaveCredentialsError> {
|
||||||
let mut cur_session = self.app_session.write().await;
|
let mut cur_session = self.app_session.write().await;
|
||||||
if let AppSession::Locked {..} = *cur_session {
|
if let AppSession::Locked {..} = *cur_session {
|
||||||
@ -302,6 +308,18 @@ impl AppState {
|
|||||||
Ok(s)
|
Ok(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn ssh_name_from_pubkey(&self, pubkey: &[u8]) -> Result<String, GetCredentialsError> {
|
||||||
|
let k = SshKey::name_from_pubkey(pubkey, &self.pool).await?;
|
||||||
|
Ok(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn sshkey_by_name(&self, name: &str) -> Result<SshKey, GetCredentialsError> {
|
||||||
|
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) {
|
pub async fn signal_activity(&self) {
|
||||||
let mut last_activity = self.last_activity.write().await;
|
let mut last_activity = self.last_activity.write().await;
|
||||||
*last_activity = OffsetDateTime::now_utc();
|
*last_activity = OffsetDateTime::now_utc();
|
||||||
|
@ -42,7 +42,13 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="space-y-1 mb-4">
|
<div class="space-y-1 mb-4">
|
||||||
<h2 class="text-xl font-bold">{appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials.</h2>
|
<h2 class="text-xl font-bold">
|
||||||
|
{#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}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-[auto_1fr] gap-x-3">
|
<div class="grid grid-cols-[auto_1fr] gap-x-3">
|
||||||
<div class="text-right">Path:</div>
|
<div class="text-right">Path:</div>
|
||||||
@ -56,7 +62,11 @@
|
|||||||
<!-- Don't display the option to approve with session credentials if base was specifically requested -->
|
<!-- Don't display the option to approve with session credentials if base was specifically requested -->
|
||||||
{#if !$appState.currentRequest?.base}
|
{#if !$appState.currentRequest?.base}
|
||||||
<h3 class="font-semibold">
|
<h3 class="font-semibold">
|
||||||
|
{#if $appState.currentRequest.type === 'Aws'}
|
||||||
Approve with session credentials
|
Approve with session credentials
|
||||||
|
{:else}
|
||||||
|
Approve
|
||||||
|
{/if}
|
||||||
</h3>
|
</h3>
|
||||||
<Link target={() => setResponse('Approved', false)} hotkey="Enter" shift={true}>
|
<Link target={() => setResponse('Approved', false)} hotkey="Enter" shift={true}>
|
||||||
<button class="w-full btn btn-success">
|
<button class="w-full btn btn-success">
|
||||||
@ -65,6 +75,7 @@
|
|||||||
</Link>
|
</Link>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if $appState.currentRequest.type === 'Aws'}
|
||||||
<h3 class="font-semibold">
|
<h3 class="font-semibold">
|
||||||
<span class="mr-2">
|
<span class="mr-2">
|
||||||
{#if $appState.currentRequest?.base}
|
{#if $appState.currentRequest?.base}
|
||||||
@ -79,6 +90,7 @@
|
|||||||
<KeyCombo keys={['Ctrl', 'Shift', 'Enter']} />
|
<KeyCombo keys={['Ctrl', 'Shift', 'Enter']} />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<h3 class="font-semibold">
|
<h3 class="font-semibold">
|
||||||
<span class="mr-2">Deny</span>
|
<span class="mr-2">Deny</span>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user