Compare commits
7 Commits
ssh-agent
...
27c2f467c4
Author | SHA1 | Date | |
---|---|---|---|
27c2f467c4 | |||
cab5ec40cc | |||
5cf848f7fe | |||
a32e36be7e | |||
10231df860 | |||
ae93a57aab | |||
9fd355b68e |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "creddy",
|
"name": "creddy",
|
||||||
"version": "0.4.9",
|
"version": "0.5.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
67
src-tauri/Cargo.lock
generated
67
src-tauri/Cargo.lock
generated
@ -1071,7 +1071,7 @@ dependencies = [
|
|||||||
"cocoa-foundation",
|
"cocoa-foundation",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
"objc",
|
"objc",
|
||||||
]
|
]
|
||||||
@ -1146,7 +1146,7 @@ dependencies = [
|
|||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"core-foundation",
|
"core-foundation",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1196,7 +1196,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "creddy"
|
name = "creddy"
|
||||||
version = "0.4.9"
|
version = "0.5.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"auto-launch",
|
"auto-launch",
|
||||||
@ -1211,13 +1211,17 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"is-terminal",
|
"is-terminal",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"openssl",
|
||||||
"rfd 0.13.0",
|
"rfd 0.13.0",
|
||||||
|
"rsa",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
"signature 2.2.0",
|
"signature 2.2.0",
|
||||||
"sodiumoxide",
|
"sodiumoxide",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"ssh-agent-lib",
|
"ssh-agent-lib",
|
||||||
|
"ssh-encoding",
|
||||||
"ssh-key",
|
"ssh-key",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
@ -1841,6 +1845,15 @@ version = "1.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||||
|
dependencies = [
|
||||||
|
"foreign-types-shared 0.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -1848,7 +1861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foreign-types-macros",
|
"foreign-types-macros",
|
||||||
"foreign-types-shared",
|
"foreign-types-shared 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1862,6 +1875,12 @@ dependencies = [
|
|||||||
"syn 2.0.68",
|
"syn 2.0.68",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types-shared"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types-shared"
|
name = "foreign-types-shared"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@ -3426,12 +3445,50 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl"
|
||||||
|
version = "0.10.64"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.6.0",
|
||||||
|
"cfg-if",
|
||||||
|
"foreign-types 0.3.2",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"openssl-macros",
|
||||||
|
"openssl-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.68",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.102"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -4788,7 +4845,7 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cfg_aliases",
|
"cfg_aliases",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"objc2",
|
"objc2",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "creddy"
|
name = "creddy"
|
||||||
version = "0.4.9"
|
version = "0.5.3"
|
||||||
description = "A friendly AWS credentials manager"
|
description = "A friendly AWS credentials manager"
|
||||||
authors = ["Joseph Montanaro"]
|
authors = ["Joseph Montanaro"]
|
||||||
license = ""
|
license = ""
|
||||||
@ -58,6 +58,10 @@ 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"] }
|
tokio-util = { version = "0.7.11", features = ["codec"] }
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
|
openssl = "0.10.64"
|
||||||
|
rsa = "0.9.6"
|
||||||
|
sha2 = "0.10.8"
|
||||||
|
ssh-encoding = "0.2.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
|
@ -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,
|
||||||
@ -53,6 +53,7 @@ pub fn run() -> tauri::Result<()> {
|
|||||||
ipc::delete_credential,
|
ipc::delete_credential,
|
||||||
ipc::list_credentials,
|
ipc::list_credentials,
|
||||||
ipc::sshkey_from_file,
|
ipc::sshkey_from_file,
|
||||||
|
ipc::sshkey_from_private_key,
|
||||||
ipc::get_config,
|
ipc::get_config,
|
||||||
ipc::save_config,
|
ipc::save_config,
|
||||||
ipc::launch_terminal,
|
ipc::launch_terminal,
|
||||||
@ -105,8 +106,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) {
|
||||||
|
@ -11,17 +11,12 @@ use std::{
|
|||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = cli::parser().get_matches();
|
let global_matches = cli::parser().get_matches();
|
||||||
if let Some(true) = args.get_one::<bool>("help") {
|
let res = match global_matches.subcommand() {
|
||||||
cli::parser().print_help().unwrap(); // if we can't print help we can't print an error
|
|
||||||
process::exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = match args.subcommand() {
|
|
||||||
None | Some(("run", _)) => launch_gui(),
|
None | Some(("run", _)) => launch_gui(),
|
||||||
Some(("get", m)) => cli::get(m),
|
Some(("get", m)) => cli::get(m, &global_matches),
|
||||||
Some(("exec", m)) => cli::exec(m),
|
Some(("exec", m)) => cli::exec(m, &global_matches),
|
||||||
Some(("shortcut", m)) => cli::invoke_shortcut(m),
|
Some(("shortcut", m)) => cli::invoke_shortcut(m, &global_matches),
|
||||||
_ => unreachable!("Unknown subcommand"),
|
_ => unreachable!("Unknown subcommand"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -35,7 +30,7 @@ fn main() {
|
|||||||
fn launch_gui() -> Result<(), CliError> {
|
fn launch_gui() -> Result<(), CliError> {
|
||||||
let mut path = env::current_exe()?;
|
let mut path = env::current_exe()?;
|
||||||
path.pop(); // bin dir
|
path.pop(); // bin dir
|
||||||
|
|
||||||
// binaries are colocated in dev, but not in production
|
// binaries are colocated in dev, but not in production
|
||||||
#[cfg(not(debug_assertions))]
|
#[cfg(not(debug_assertions))]
|
||||||
path.pop(); // install dir
|
path.pop(); // install dir
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::process::Command as ChildCommand;
|
use std::process::Command as ChildCommand;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@ -9,11 +10,16 @@ use clap::{
|
|||||||
ArgMatches,
|
ArgMatches,
|
||||||
ArgAction,
|
ArgAction,
|
||||||
builder::PossibleValuesParser,
|
builder::PossibleValuesParser,
|
||||||
|
value_parser,
|
||||||
};
|
};
|
||||||
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)]
|
||||||
@ -33,6 +39,14 @@ pub fn parser() -> Command<'static> {
|
|||||||
Command::new("creddy")
|
Command::new("creddy")
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.about("A friendly AWS credentials manager")
|
.about("A friendly AWS credentials manager")
|
||||||
|
.arg(
|
||||||
|
Arg::new("server_addr")
|
||||||
|
.short('a')
|
||||||
|
.long("server-addr")
|
||||||
|
.takes_value(true)
|
||||||
|
.value_parser(value_parser!(PathBuf))
|
||||||
|
.help("Connect to the main Creddy process at this address")
|
||||||
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("run")
|
Command::new("run")
|
||||||
.about("Launch Creddy")
|
.about("Launch Creddy")
|
||||||
@ -47,6 +61,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 +77,13 @@ 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")
|
||||||
|
.takes_value(true)
|
||||||
|
.help("If unspecified, use default credentials")
|
||||||
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("command")
|
Arg::new("command")
|
||||||
.multiple_values(true)
|
.multiple_values(true)
|
||||||
@ -77,9 +102,12 @@ pub fn parser() -> Command<'static> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn get(args: &ArgMatches) -> Result<(), CliError> {
|
pub fn get(args: &ArgMatches, global_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 addr = global_args.get_one("server_addr").cloned();
|
||||||
|
|
||||||
|
let output = match make_request(addr, &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()),
|
||||||
@ -89,16 +117,18 @@ pub fn get(args: &ArgMatches) -> Result<(), CliError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
|
pub fn exec(args: &ArgMatches, global_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 addr = global_args.get_one("server_addr").cloned();
|
||||||
let mut cmd_line = args.get_many("command")
|
let mut cmd_line = args.get_many("command")
|
||||||
.ok_or(ExecError::NoCommand)?;
|
.ok_or(ExecError::NoCommand)?;
|
||||||
|
|
||||||
let cmd_name: &String = cmd_line.next().unwrap(); // Clap guarantees that there will be at least one
|
let cmd_name: &String = cmd_line.next().unwrap(); // Clap guarantees that there will be at least one
|
||||||
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(addr, &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);
|
||||||
@ -142,7 +172,8 @@ pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
|
pub fn invoke_shortcut(args: &ArgMatches, global_args: &ArgMatches) -> Result<(), CliError> {
|
||||||
|
let addr = global_args.get_one("server_addr").cloned();
|
||||||
let action = match args.get_one::<String>("action").map(|s| s.as_str()) {
|
let action = match args.get_one::<String>("action").map(|s| s.as_str()) {
|
||||||
Some("show_window") => ShortcutAction::ShowWindow,
|
Some("show_window") => ShortcutAction::ShowWindow,
|
||||||
Some("launch_terminal") => ShortcutAction::LaunchTerminal,
|
Some("launch_terminal") => ShortcutAction::LaunchTerminal,
|
||||||
@ -150,7 +181,7 @@ pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let req = Request::InvokeShortcut(action);
|
let req = Request::InvokeShortcut(action);
|
||||||
match make_request(&req) {
|
match make_request(addr, &req) {
|
||||||
Ok(Response::Empty) => Ok(()),
|
Ok(Response::Empty) => Ok(()),
|
||||||
Ok(r) => Err(RequestError::Unexpected(r).into()),
|
Ok(r) => Err(RequestError::Unexpected(r).into()),
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(e.into()),
|
||||||
@ -159,12 +190,12 @@ pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
|
|||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn make_request(req: &Request) -> Result<Response, RequestError> {
|
async fn make_request(addr: Option<PathBuf>, req: &Request) -> Result<Response, RequestError> {
|
||||||
let mut data = serde_json::to_string(req).unwrap();
|
let mut data = serde_json::to_string(req).unwrap();
|
||||||
// server expects newline marking end of request
|
// server expects newline marking end of request
|
||||||
data.push('\n');
|
data.push('\n');
|
||||||
|
|
||||||
let mut stream = connect().await?;
|
let mut stream = connect(addr).await?;
|
||||||
stream.write_all(&data.as_bytes()).await?;
|
stream.write_all(&data.as_bytes()).await?;
|
||||||
|
|
||||||
let mut buf = Vec::with_capacity(1024);
|
let mut buf = Vec::with_capacity(1024);
|
||||||
@ -175,10 +206,11 @@ async fn make_request(req: &Request) -> Result<Response, RequestError> {
|
|||||||
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
async fn connect() -> Result<NamedPipeClient, std::io::Error> {
|
async fn connect(addr: Option<PathBuf>) -> 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 = addr.unwrap_or_else(|| 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),
|
||||||
@ -189,6 +221,7 @@ async fn connect() -> Result<NamedPipeClient, std::io::Error> {
|
|||||||
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
async fn connect() -> Result<UnixStream, std::io::Error> {
|
async fn connect(addr: Option<PathBuf>) -> Result<UnixStream, std::io::Error> {
|
||||||
UnixStream::connect("/tmp/creddy.sock").await
|
let path = addr.unwrap_or_else(|| srv::addr("creddy-server"));
|
||||||
|
UnixStream::connect(&path).await
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ impl PersistentCredential for AwsBaseCredential {
|
|||||||
access_key_id,
|
access_key_id,
|
||||||
secret_key_enc,
|
secret_key_enc,
|
||||||
nonce
|
nonce
|
||||||
)
|
)
|
||||||
VALUES (?, ?, ?, ?);",
|
VALUES (?, ?, ?, ?);",
|
||||||
id, self.access_key_id, ciphertext, nonce_bytes,
|
id, self.access_key_id, ciphertext, nonce_bytes,
|
||||||
).execute(&mut **txn).await?;
|
).execute(&mut **txn).await?;
|
||||||
@ -203,19 +203,6 @@ mod tests {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_uuid() -> Uuid {
|
|
||||||
Uuid::try_parse("00000000-0000-0000-0000-000000000000").unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_uuid_2() -> Uuid {
|
|
||||||
Uuid::try_parse("ffffffff-ffff-ffff-ffff-ffffffffffff").unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn test_uuid_random() -> Uuid {
|
|
||||||
let bytes = Crypto::salt();
|
|
||||||
Uuid::from_slice(&bytes[..16]).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[sqlx::test(fixtures("aws_credentials"))]
|
#[sqlx::test(fixtures("aws_credentials"))]
|
||||||
async fn test_load(pool: SqlitePool) {
|
async fn test_load(pool: SqlitePool) {
|
||||||
@ -254,5 +241,5 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(&creds().into_credential(), &list[0]);
|
assert_eq!(&creds().into_credential(), &list[0]);
|
||||||
assert_eq!(&creds_2().into_credential(), &list[1]);
|
assert_eq!(&creds_2().into_credential(), &list[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,19 +112,30 @@ impl CredentialRecord {
|
|||||||
Ok(Self::from_parts(row, credential))
|
Ok(Self::from_parts(row, credential))
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
#[cfg(test)]
|
||||||
// let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE id = ?")
|
pub async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||||
// .bind(id)
|
let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE id = ?")
|
||||||
// .fetch_optional(pool)
|
.bind(id)
|
||||||
// .await?
|
.fetch_optional(pool)
|
||||||
// .ok_or(LoadCredentialsError::NoCredentials)?;
|
.await?
|
||||||
|
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||||
|
|
||||||
// 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
|
||||||
WHERE credential_type = ? AND is_default = 1"
|
WHERE credential_type = ? AND is_default = 1"
|
||||||
).bind(credential_type)
|
).bind(credential_type)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
@ -409,7 +420,7 @@ mod uuid_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_serialize_deserialize_uuid() {
|
fn test_serialize_deserialize_uuid() {
|
||||||
let buf = Crypto::salt();
|
let buf = Crypto::salt();
|
||||||
let expected = UuidWrapper{
|
let expected = UuidWrapper{
|
||||||
id: Uuid::from_slice(&buf[..16]).unwrap()
|
id: Uuid::from_slice(&buf[..16]).unwrap()
|
||||||
};
|
};
|
||||||
let serialized = serde_json::to_string(&expected).unwrap();
|
let serialized = serde_json::to_string(&expected).unwrap();
|
||||||
|
@ -12,6 +12,8 @@ use serde::ser::{
|
|||||||
SerializeStruct,
|
SerializeStruct,
|
||||||
};
|
};
|
||||||
use serde::de::{self, Visitor};
|
use serde::de::{self, Visitor};
|
||||||
|
use sha2::{Sha256, Sha512};
|
||||||
|
use signature::{Signer, SignatureEncoding};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
FromRow,
|
FromRow,
|
||||||
Sqlite,
|
Sqlite,
|
||||||
@ -19,11 +21,15 @@ use sqlx::{
|
|||||||
Transaction,
|
Transaction,
|
||||||
types::Uuid,
|
types::Uuid,
|
||||||
};
|
};
|
||||||
use ssh_agent_lib::proto::message::Identity;
|
use ssh_agent_lib::proto::message::{
|
||||||
|
Identity,
|
||||||
|
SignRequest,
|
||||||
|
};
|
||||||
|
use ssh_encoding::Encode;
|
||||||
use ssh_key::{
|
use ssh_key::{
|
||||||
Algorithm,
|
Algorithm,
|
||||||
LineEnding,
|
LineEnding,
|
||||||
private::PrivateKey,
|
private::{PrivateKey, KeypairData},
|
||||||
public::PublicKey,
|
public::PublicKey,
|
||||||
};
|
};
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
@ -74,11 +80,26 @@ impl SshKey {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_private_key(private_key: &str, passphrase: &str) -> Result<SshKey, LoadSshKeyError> {
|
||||||
|
let mut privkey = PrivateKey::from_openssh(private_key)?;
|
||||||
|
if privkey.is_encrypted() {
|
||||||
|
privkey = privkey.decrypt(passphrase)
|
||||||
|
.map_err(|_| LoadSshKeyError::InvalidPassphrase)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SshKey {
|
||||||
|
algorithm: privkey.algorithm(),
|
||||||
|
comment: privkey.comment().into(),
|
||||||
|
public_key: privkey.public_key().clone(),
|
||||||
|
private_key: privkey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn name_from_pubkey(pubkey: &[u8], pool: &SqlitePool) -> Result<String, LoadCredentialsError> {
|
pub async fn name_from_pubkey(pubkey: &[u8], pool: &SqlitePool) -> Result<String, LoadCredentialsError> {
|
||||||
let row = sqlx::query!(
|
let row = sqlx::query!(
|
||||||
"SELECT c.name
|
"SELECT c.name
|
||||||
FROM credentials c
|
FROM credentials c
|
||||||
JOIN ssh_credentials s
|
JOIN ssh_credentials s
|
||||||
ON s.id = c.id
|
ON s.id = c.id
|
||||||
WHERE s.public_key = ?",
|
WHERE s.public_key = ?",
|
||||||
pubkey
|
pubkey
|
||||||
@ -104,6 +125,33 @@ impl SshKey {
|
|||||||
|
|
||||||
Ok(identities)
|
Ok(identities)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn sign_request(&self, req: &SignRequest) -> Result<Vec<u8>, HandlerError> {
|
||||||
|
let mut sig = Vec::new();
|
||||||
|
match self.private_key.key_data() {
|
||||||
|
KeypairData::Rsa(keypair) => {
|
||||||
|
// 2 is the flag value for `SSH_AGENT_RSA_SHA2_256`
|
||||||
|
if req.flags & 2 > 0 {
|
||||||
|
let signer = rsa::pkcs1v15::SigningKey::<Sha256>::try_from(keypair)?;
|
||||||
|
let sig_data = signer.try_sign(&req.data)?.to_vec();
|
||||||
|
"rsa-sha-256".encode(&mut sig)?;
|
||||||
|
sig_data.encode(&mut sig)?;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let signer = rsa::pkcs1v15::SigningKey::<Sha512>::try_from(keypair)?;
|
||||||
|
let sig_data = signer.try_sign(&req.data)?.to_vec();
|
||||||
|
"rsa-sha2-512".encode(&mut sig)?;
|
||||||
|
sig_data.encode(&mut sig)?;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
let sig_data = self.private_key.try_sign(&req.data)?;
|
||||||
|
self.algorithm.as_str().encode(&mut sig)?;
|
||||||
|
sig_data.as_bytes().encode(&mut sig)?;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Ok(sig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -120,7 +168,7 @@ impl PersistentCredential for SshKey {
|
|||||||
let nonce = XNonce::clone_from_slice(&row.nonce);
|
let nonce = XNonce::clone_from_slice(&row.nonce);
|
||||||
let privkey_bytes = crypto.decrypt(&nonce, &row.private_key_enc)?;
|
let privkey_bytes = crypto.decrypt(&nonce, &row.private_key_enc)?;
|
||||||
|
|
||||||
|
|
||||||
let algorithm = Algorithm::new(&row.algorithm)
|
let algorithm = Algorithm::new(&row.algorithm)
|
||||||
.map_err(|_| LoadCredentialsError::InvalidData)?;
|
.map_err(|_| LoadCredentialsError::InvalidData)?;
|
||||||
let public_key = PublicKey::from_bytes(&row.public_key)
|
let public_key = PublicKey::from_bytes(&row.public_key)
|
||||||
@ -250,7 +298,6 @@ fn deserialize_algorithm<'de, D>(deserializer: D) -> Result<Algorithm, D::Error>
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use ssh_key::Fingerprint;
|
|
||||||
use sqlx::types::uuid::uuid;
|
use sqlx::types::uuid::uuid;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@ -293,7 +340,7 @@ mod tests {
|
|||||||
let k = rsa_plain();
|
let k = rsa_plain();
|
||||||
assert_eq!(k.algorithm.as_str(), "ssh-rsa");
|
assert_eq!(k.algorithm.as_str(), "ssh-rsa");
|
||||||
assert_eq!(&k.comment, "hello world");
|
assert_eq!(&k.comment, "hello world");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
k.public_key.fingerprint(Default::default()),
|
k.public_key.fingerprint(Default::default()),
|
||||||
k.private_key.fingerprint(Default::default()),
|
k.private_key.fingerprint(Default::default()),
|
||||||
@ -311,7 +358,7 @@ mod tests {
|
|||||||
let k = rsa_enc();
|
let k = rsa_enc();
|
||||||
assert_eq!(k.algorithm.as_str(), "ssh-rsa");
|
assert_eq!(k.algorithm.as_str(), "ssh-rsa");
|
||||||
assert_eq!(&k.comment, "hello world");
|
assert_eq!(&k.comment, "hello world");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
k.public_key.fingerprint(Default::default()),
|
k.public_key.fingerprint(Default::default()),
|
||||||
k.private_key.fingerprint(Default::default()),
|
k.private_key.fingerprint(Default::default()),
|
||||||
@ -329,7 +376,7 @@ mod tests {
|
|||||||
let k = ed25519_plain();
|
let k = ed25519_plain();
|
||||||
assert_eq!(k.algorithm.as_str(),"ssh-ed25519");
|
assert_eq!(k.algorithm.as_str(),"ssh-ed25519");
|
||||||
assert_eq!(&k.comment, "hello world");
|
assert_eq!(&k.comment, "hello world");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
k.public_key.fingerprint(Default::default()),
|
k.public_key.fingerprint(Default::default()),
|
||||||
k.private_key.fingerprint(Default::default()),
|
k.private_key.fingerprint(Default::default()),
|
||||||
@ -347,7 +394,7 @@ mod tests {
|
|||||||
let k = ed25519_enc();
|
let k = ed25519_enc();
|
||||||
assert_eq!(k.algorithm.as_str(), "ssh-ed25519");
|
assert_eq!(k.algorithm.as_str(), "ssh-ed25519");
|
||||||
assert_eq!(&k.comment, "hello world");
|
assert_eq!(&k.comment, "hello world");
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
k.public_key.fingerprint(Default::default()),
|
k.public_key.fingerprint(Default::default()),
|
||||||
k.private_key.fingerprint(Default::default()),
|
k.private_key.fingerprint(Default::default()),
|
||||||
@ -399,7 +446,7 @@ mod tests {
|
|||||||
async fn test_load_db(pool: SqlitePool) {
|
async fn test_load_db(pool: SqlitePool) {
|
||||||
let crypto = Crypto::fixed();
|
let crypto = Crypto::fixed();
|
||||||
let id = uuid!("11111111-1111-1111-1111-111111111111");
|
let id = uuid!("11111111-1111-1111-1111-111111111111");
|
||||||
let k = SshKey::load(&id, &crypto, &pool).await
|
SshKey::load(&id, &crypto, &pool).await
|
||||||
.expect("Failed to load SSH key from database");
|
.expect("Failed to load SSH key from database");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,6 +195,10 @@ pub enum HandlerError {
|
|||||||
SshAgent(#[from] ssh_agent_lib::error::AgentError),
|
SshAgent(#[from] ssh_agent_lib::error::AgentError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
SshKey(#[from] ssh_key::Error),
|
SshKey(#[from] ssh_key::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Signature(#[from] signature::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Encoding(#[from] ssh_encoding::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -338,6 +342,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 +370,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}")]
|
||||||
|
@ -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 {
|
||||||
@ -141,6 +142,12 @@ pub async fn sshkey_from_file(path: &str, passphrase: &str) -> Result<SshKey, Lo
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn sshkey_from_private_key(private_key: &str, passphrase: &str) -> Result<SshKey, LoadSshKeyError> {
|
||||||
|
SshKey::from_private_key(private_key, passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> {
|
pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> {
|
||||||
let config = app_state.config.read().await;
|
let config = app_state.config.read().await;
|
||||||
|
@ -44,21 +44,23 @@ pub async fn load_bytes(pool: &SqlitePool, name: &str) -> Result<Option<Vec<u8>>
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
|
// we don't have a need for this right now, but we will some day
|
||||||
// sqlx::query!("DELETE FROM kv WHERE name = ?", name)
|
#[cfg(test)]
|
||||||
// .execute(pool)
|
pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
|
||||||
// .await?;
|
sqlx::query!("DELETE FROM kv WHERE name = ?", name)
|
||||||
// Ok(())
|
.execute(pool)
|
||||||
// }
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn delete_multi(pool: &SqlitePool, names: &[&str]) -> Result<(), sqlx::Error> {
|
pub async fn delete_multi(pool: &SqlitePool, names: &[&str]) -> Result<(), sqlx::Error> {
|
||||||
let placeholder = names.iter()
|
let placeholder = names.iter()
|
||||||
.map(|_| "?")
|
.map(|_| "?")
|
||||||
.collect::<Vec<&str>>()
|
.collect::<Vec<&str>>()
|
||||||
.join(",");
|
.join(",");
|
||||||
let query = format!("DELETE FROM kv WHERE name IN ({})", placeholder);
|
let query = format!("DELETE FROM kv WHERE name IN ({})", placeholder);
|
||||||
|
|
||||||
let mut q = sqlx::query(&query);
|
let mut q = sqlx::query(&query);
|
||||||
for name in names {
|
for name in names {
|
||||||
q = q.bind(name);
|
q = q.bind(name);
|
||||||
@ -83,7 +85,7 @@ macro_rules! load_bytes_multi {
|
|||||||
(
|
(
|
||||||
// ...with one item for each repetition of $name
|
// ...with one item for each repetition of $name
|
||||||
$(
|
$(
|
||||||
// load_bytes returns Result<Option<_>>, the Result is handled by
|
// load_bytes returns Result<Option<_>>, the Result is handled by
|
||||||
// the ? and we match on the Option
|
// the ? and we match on the Option
|
||||||
match crate::kv::load_bytes($pool, $name).await? {
|
match crate::kv::load_bytes($pool, $name).await? {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
@ -187,7 +189,7 @@ mod tests {
|
|||||||
async fn test_delete(pool: SqlitePool) {
|
async fn test_delete(pool: SqlitePool) {
|
||||||
delete(&pool, "test_bytes").await
|
delete(&pool, "test_bytes").await
|
||||||
.expect("Failed to delete data");
|
.expect("Failed to delete data");
|
||||||
|
|
||||||
let loaded = load_bytes(&pool, "test_bytes").await
|
let loaded = load_bytes(&pool, "test_bytes").await
|
||||||
.expect("Failed to load data");
|
.expect("Failed to load data");
|
||||||
assert_eq!(loaded, None);
|
assert_eq!(loaded, None);
|
||||||
|
@ -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;
|
||||||
|
@ -11,14 +11,15 @@ use creddy::{
|
|||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let res = match cli::parser().get_matches().subcommand() {
|
let global_matches = cli::parser().get_matches();
|
||||||
|
let res = match global_matches.subcommand() {
|
||||||
None | Some(("run", _)) => {
|
None | Some(("run", _)) => {
|
||||||
app::run().error_popup("Creddy encountered an error");
|
app::run().error_popup("Creddy encountered an error");
|
||||||
Ok(())
|
Ok(())
|
||||||
},
|
},
|
||||||
Some(("get", m)) => cli::get(m),
|
Some(("get", m)) => cli::get(m, &global_matches),
|
||||||
Some(("exec", m)) => cli::exec(m),
|
Some(("exec", m)) => cli::exec(m, &global_matches),
|
||||||
Some(("shortcut", m)) => cli::invoke_shortcut(m),
|
Some(("shortcut", m)) => cli::invoke_shortcut(m, &global_matches),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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();
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
use std::io::ErrorKind;
|
|
||||||
|
|
||||||
use futures::SinkExt;
|
|
||||||
use signature::Signer;
|
|
||||||
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::errors::*;
|
|
||||||
use crate::ipc::{Approval, RequestNotification};
|
|
||||||
use crate::state::AppState;
|
|
||||||
|
|
||||||
|
|
||||||
pub struct Agent {
|
|
||||||
listener: UnixListener,
|
|
||||||
app_handle: AppHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async fn handle_framed(
|
|
||||||
mut adapter: Framed<UnixStream, MessageCodec>,
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
adapter.send(resp).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async fn sign_request(req: SignRequest, app_handle: AppHandle, client_pid: u32) -> Result<Message, HandlerError> {
|
|
||||||
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());
|
|
||||||
|
|
||||||
Ok(Message::SignResponse(payload))
|
|
||||||
};
|
|
||||||
|
|
||||||
let res = proceed.await;
|
|
||||||
if let Err(_) = &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)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn encode_string(buf: &mut Vec<u8>, s: &[u8]) {
|
|
||||||
let len = s.len() as u32;
|
|
||||||
buf.extend(len.to_be_bytes());
|
|
||||||
buf.extend(s);
|
|
||||||
}
|
|
115
src-tauri/src/srv/agent.rs
Normal file
115
src-tauri/src/srv/agent.rs
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
use futures::SinkExt;
|
||||||
|
use ssh_agent_lib::agent::MessageCodec;
|
||||||
|
use ssh_agent_lib::proto::message::{
|
||||||
|
Message,
|
||||||
|
SignRequest,
|
||||||
|
};
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
use tokio_util::codec::Framed;
|
||||||
|
|
||||||
|
use crate::clientinfo;
|
||||||
|
use crate::errors::*;
|
||||||
|
use crate::ipc::{Approval, RequestNotification};
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
use super::{CloseWaiter, Stream};
|
||||||
|
|
||||||
|
|
||||||
|
pub fn serve(app_handle: AppHandle) -> std::io::Result<()> {
|
||||||
|
super::serve("creddy-agent", app_handle, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn handle(
|
||||||
|
stream: Stream,
|
||||||
|
app_handle: AppHandle,
|
||||||
|
client_pid: u32
|
||||||
|
) -> Result<(), HandlerError> {
|
||||||
|
let mut adapter = Framed::new(stream, MessageCodec);
|
||||||
|
while let Some(message) = adapter.try_next().await? {
|
||||||
|
match message {
|
||||||
|
Message::RequestIdentities => {
|
||||||
|
let resp = list_identities(app_handle.clone()).await?;
|
||||||
|
adapter.send(resp).await?;
|
||||||
|
},
|
||||||
|
Message::SignRequest(req) => {
|
||||||
|
// Note: If the client writes more data to the stream *while* at the
|
||||||
|
// same time waiting for a resopnse to a previous request, this will
|
||||||
|
// corrupt the framing. Clients don't seem to behave that way though?
|
||||||
|
let waiter = CloseWaiter { stream: adapter.get_mut() };
|
||||||
|
let resp = sign_request(req, app_handle.clone(), client_pid, waiter).await?;
|
||||||
|
|
||||||
|
// have to do this before we send since we can't inspect the message after
|
||||||
|
let is_failure = matches!(resp, Message::Failure);
|
||||||
|
adapter.send(resp).await?;
|
||||||
|
|
||||||
|
if is_failure {
|
||||||
|
// this way we don't get spammed with requests for other keys
|
||||||
|
// after denying the first
|
||||||
|
break
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => adapter.send(Message::Failure).await?,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn list_identities(app_handle: AppHandle) -> Result<Message, HandlerError> {
|
||||||
|
let state = app_handle.state::<AppState>();
|
||||||
|
let identities = state.list_ssh_identities().await?;
|
||||||
|
Ok(Message::IdentitiesAnswer(identities))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 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 = tokio::select! {
|
||||||
|
r = chan_recv => r?,
|
||||||
|
_ = waiter.wait_for_close() => {
|
||||||
|
app_handle.emit("request-cancelled", request_id)?;
|
||||||
|
return Err(HandlerError::Abandoned);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Approval::Denied = response.approval {
|
||||||
|
return Ok(Message::Failure);
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = state.sshkey_by_name(&key_name).await?;
|
||||||
|
let sig = key.sign_request(&req)?;
|
||||||
|
Ok(Message::SignResponse(sig))
|
||||||
|
};
|
||||||
|
|
||||||
|
let res = proceed.await;
|
||||||
|
if let Err(_) = &res {
|
||||||
|
state.unregister_request(request_id).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
lease.release();
|
||||||
|
res
|
||||||
|
}
|
@ -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", ¬ification)?;
|
app_handle.emit("credential-request", ¬ification)?;
|
||||||
|
|
||||||
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
170
src-tauri/src/srv/mod.rs
Normal 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}")
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"productName": "creddy",
|
"productName": "creddy",
|
||||||
"version": "0.4.9",
|
"version": "0.5.3",
|
||||||
"identifier": "creddy",
|
"identifier": "creddy",
|
||||||
"plugins": {},
|
"plugins": {},
|
||||||
"app": {
|
"app": {
|
||||||
@ -85,4 +85,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
|
|
||||||
// Extra 50ms so the window can finish disappearing before the redraw
|
// Extra 50ms so the window can finish disappearing before the redraw
|
||||||
const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 50);
|
const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 100);
|
||||||
|
|
||||||
let alert;
|
let alert;
|
||||||
let success = false;
|
let success = false;
|
||||||
|
@ -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">
|
||||||
|
@ -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}
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
async function saveCredential() {
|
async function saveCredential() {
|
||||||
await invoke('save_credential', {record: local});
|
await invoke('save_credential', {record: local});
|
||||||
dispatch('save', local);
|
dispatch('save', local);
|
||||||
showDetails = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyText(evt) {
|
async function copyText(evt) {
|
||||||
|
@ -13,20 +13,23 @@
|
|||||||
|
|
||||||
let name;
|
let name;
|
||||||
let file;
|
let file;
|
||||||
|
let privateKey = '';
|
||||||
let passphrase = '';
|
let passphrase = '';
|
||||||
let showDetails = true;
|
let showDetails = true;
|
||||||
|
let mode = 'file';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
let defaultPath = null;
|
let defaultPath = null;
|
||||||
homeDir().then(d => defaultPath = `${d}/.ssh`);
|
homeDir().then(d => defaultPath = `${d}/.ssh`);
|
||||||
|
|
||||||
|
|
||||||
let alert;
|
let alert;
|
||||||
let saving = false;
|
let saving = false;
|
||||||
async function saveCredential() {
|
async function saveCredential() {
|
||||||
saving = true;
|
saving = true;
|
||||||
try {
|
try {
|
||||||
let key = await invoke('sshkey_from_file', {path: file.path, passphrase});
|
let key = await getKey();
|
||||||
const payload = {
|
const payload = {
|
||||||
id: record.id,
|
id: record.id,
|
||||||
name,
|
name,
|
||||||
@ -41,9 +44,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getKey() {
|
||||||
|
if (mode === 'file') {
|
||||||
|
return await invoke('sshkey_from_file', {path: file.path, passphrase});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return await invoke('sshkey_from_private_key', {privateKey, passphrase});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div role="tablist" class="join max-w-sm mx-auto flex justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
class="join-item flex-1 btn border border-primary hover:border-primary"
|
||||||
|
class:btn-primary={mode === 'file'}
|
||||||
|
on:click={() => mode = 'file'}
|
||||||
|
>
|
||||||
|
From file
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
class="join-item flex-1 btn border border-primary hover:border-primary"
|
||||||
|
class:btn-primary={mode === 'direct'}
|
||||||
|
on:click={() => mode = 'direct'}
|
||||||
|
>
|
||||||
|
From private key
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="space-y-4" on:submit|preventDefault={alert.run(saveCredential)}>
|
<form class="space-y-4" on:submit|preventDefault={alert.run(saveCredential)}>
|
||||||
<ErrorAlert bind:this={alert} />
|
<ErrorAlert bind:this={alert} />
|
||||||
|
|
||||||
@ -55,15 +89,20 @@
|
|||||||
bind:value={name}
|
bind:value={name}
|
||||||
>
|
>
|
||||||
|
|
||||||
<span class="justify-self-end">File</span>
|
{#if mode === 'file'}
|
||||||
<FileInput params={{defaultPath}} bind:value={file} on:update={() => name = file.name} />
|
<span class="justify-self-end">File</span>
|
||||||
|
<FileInput params={{defaultPath}} bind:value={file} on:update={() => name = file.name} />
|
||||||
|
{:else}
|
||||||
|
<span class="justify-self-end">Private key</span>
|
||||||
|
<textarea bind:value={privateKey} rows="5" class="textarea textarea-bordered bg-transparent font-mono whitespace-pre overflow-x-auto"></textarea>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<span class="justify-self-end">Passphrase</span>
|
<span class="justify-self-end">Passphrase</span>
|
||||||
<PassphraseInput class="bg-transparent" bind:value={passphrase} />
|
<PassphraseInput class="bg-transparent" bind:value={passphrase} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
{#if file?.path}
|
{#if file?.path || privateKey !== ''}
|
||||||
<button
|
<button
|
||||||
transition:fade={{duration: 100}}
|
transition:fade={{duration: 100}}
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -64,7 +64,7 @@
|
|||||||
{#if record.isNew}
|
{#if record.isNew}
|
||||||
<NewSshKey {record} on:save on:save={handleSave} />
|
<NewSshKey {record} on:save on:save={handleSave} />
|
||||||
{:else}
|
{:else}
|
||||||
<EditSshKey bind:local={local} {isModified} on:save />
|
<EditSshKey bind:local={local} {isModified} on:save={handleSave} on:save />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
Reference in New Issue
Block a user