Compare commits
No commits in common. "master" and "ssh-agent" have entirely different histories.
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "creddy",
|
||||
"version": "0.5.4",
|
||||
"version": "0.4.9",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
241
src-tauri/Cargo.lock
generated
241
src-tauri/Cargo.lock
generated
@ -110,55 +110,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.86"
|
||||
@ -376,6 +327,17 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi 0.1.19",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "auto-launch"
|
||||
version = "0.4.0"
|
||||
@ -1061,43 +1023,42 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.9"
|
||||
version = "3.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
|
||||
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"atty",
|
||||
"bitflags 1.3.2",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
"indexmap 1.9.3",
|
||||
"once_cell",
|
||||
"strsim 0.10.0",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.8"
|
||||
version = "3.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
|
||||
checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"heck 0.4.1",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.1"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
|
||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
@ -1110,7 +1071,7 @@ dependencies = [
|
||||
"cocoa-foundation",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"foreign-types 0.5.0",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
@ -1129,12 +1090,6 @@ dependencies = [
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@ -1191,7 +1146,7 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types 0.5.0",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@ -1241,7 +1196,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "creddy"
|
||||
version = "0.5.4"
|
||||
version = "0.4.9"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"auto-launch",
|
||||
@ -1249,23 +1204,20 @@ dependencies = [
|
||||
"aws-sdk-sts",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"base64 0.22.1",
|
||||
"chacha20poly1305",
|
||||
"creddy_cli",
|
||||
"clap",
|
||||
"dirs 5.0.1",
|
||||
"futures",
|
||||
"is-terminal",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"rfd 0.13.0",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"signature 2.2.0",
|
||||
"sodiumoxide",
|
||||
"sqlx",
|
||||
"ssh-agent-lib",
|
||||
"ssh-encoding",
|
||||
"ssh-key",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
@ -1285,18 +1237,6 @@ dependencies = [
|
||||
"windows 0.51.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "creddy_cli"
|
||||
version = "0.5.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"dirs 5.0.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.13"
|
||||
@ -1455,7 +1395,7 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"strsim 0.11.1",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
@ -1901,15 +1841,6 @@ version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "foreign-types"
|
||||
version = "0.5.0"
|
||||
@ -1917,7 +1848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared 0.3.1",
|
||||
"foreign-types-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1931,12 +1862,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "foreign-types-shared"
|
||||
version = "0.3.1"
|
||||
@ -2491,6 +2416,15 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
@ -2813,12 +2747,6 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.8"
|
||||
@ -3498,50 +3426,12 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
@ -3569,6 +3459,12 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
version = "0.5.1"
|
||||
@ -4666,9 +4562,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.120"
|
||||
version = "1.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
|
||||
checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
|
||||
dependencies = [
|
||||
"itoa 1.0.11",
|
||||
"ryu",
|
||||
@ -4892,7 +4788,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"foreign-types 0.5.0",
|
||||
"foreign-types",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
@ -5285,6 +5181,12 @@ dependencies = [
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
@ -5772,6 +5674,21 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
|
||||
|
||||
[[package]]
|
||||
name = "thin-slice"
|
||||
version = "0.1.1"
|
||||
@ -6243,12 +6160,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.9.1"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "creddy"
|
||||
version = "0.5.4"
|
||||
version = "0.4.9"
|
||||
description = "A friendly AWS credentials manager"
|
||||
authors = ["Joseph Montanaro"]
|
||||
license = ""
|
||||
@ -9,40 +9,37 @@ default-run = "creddy"
|
||||
edition = "2021"
|
||||
rust-version = "1.57"
|
||||
|
||||
[[bin]]
|
||||
name = "creddy_cli"
|
||||
path = "src/bin/creddy_cli.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "creddy"
|
||||
path = "src/main.rs"
|
||||
|
||||
# we use a workspace so that we can split out the CLI and make it possible to build independently
|
||||
[workspace]
|
||||
members = ["creddy_cli"]
|
||||
|
||||
[workspace.dependencies]
|
||||
dirs = "5.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = ">=1.19", features = ["full"] }
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-beta", features = [] }
|
||||
|
||||
[dependencies]
|
||||
creddy_cli = { path = "./creddy_cli" }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tauri = { version = "2.0.0-beta", features = ["tray-icon"] }
|
||||
sodiumoxide = "0.2.7"
|
||||
tokio = { version = ">=1.19", features = ["full"] }
|
||||
sysinfo = "0.26.8"
|
||||
aws-config = "1.5.3"
|
||||
aws-types = "1.3.2"
|
||||
aws-sdk-sts = "1.33.0"
|
||||
aws-smithy-types = "1.2.0"
|
||||
dirs = { workspace = true }
|
||||
thiserror = "1.0.38"
|
||||
once_cell = "1.16.0"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
auto-launch = "0.4.0"
|
||||
dirs = "5.0"
|
||||
clap = { version = "3.2.23", features = ["derive"] }
|
||||
is-terminal = "0.4.7"
|
||||
argon2 = { version = "0.5.0", features = ["std"] }
|
||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||
@ -58,16 +55,9 @@ ssh-agent-lib = "0.4.0"
|
||||
ssh-key = { version = "0.6.6", features = ["rsa", "ed25519", "encryption"] }
|
||||
signature = "2.2.0"
|
||||
tokio-stream = "0.1.15"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "uuid"] }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { version = "0.7.11", features = ["codec"] }
|
||||
futures = "0.3.30"
|
||||
openssl = "0.10.64"
|
||||
rsa = "0.9.6"
|
||||
sha2 = "0.10.8"
|
||||
ssh-encoding = "0.2.0"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
@ -77,5 +67,8 @@ default = ["custom-protocol"]
|
||||
# DO NOT remove this
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[dev-dependencies]
|
||||
base64 = "0.22.1"
|
||||
|
||||
# [profile.dev.build-override]
|
||||
# opt-level = 3
|
||||
|
@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "creddy_cli"
|
||||
version = "0.5.4"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.86"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
dirs = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
@ -1,208 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command as ChildCommand;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::CommandExt;
|
||||
#[cfg(windows)]
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use clap::{
|
||||
Args,
|
||||
Parser,
|
||||
Subcommand
|
||||
};
|
||||
use clap::builder::styling::{Styles, AnsiColor};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::proto::{
|
||||
CliCredential,
|
||||
CliRequest,
|
||||
CliResponse,
|
||||
ServerError,
|
||||
ShortcutAction,
|
||||
};
|
||||
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
about,
|
||||
version,
|
||||
name = "creddy",
|
||||
bin_name = "creddy",
|
||||
styles = Styles::styled()
|
||||
.header(AnsiColor::Yellow.on_default())
|
||||
.usage(AnsiColor::Yellow.on_default())
|
||||
.literal(AnsiColor::Green.on_default())
|
||||
.placeholder(AnsiColor::Green.on_default())
|
||||
)]
|
||||
/// A friendly credential manager
|
||||
pub struct Cli {
|
||||
#[command(flatten)]
|
||||
pub global_args: GlobalArgs,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub action: Option<Action>,
|
||||
}
|
||||
|
||||
impl Cli {
|
||||
// proxy the Parser method so that main crate doesn't have to depend on Clap
|
||||
pub fn parse() -> Self {
|
||||
<Self as Parser>::parse()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Args)]
|
||||
pub struct GlobalArgs {
|
||||
/// Connect to the main Creddy application at this path
|
||||
#[arg(long, short = 'a')]
|
||||
server_addr: Option<PathBuf>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum Action {
|
||||
/// Launch Creddy
|
||||
Run,
|
||||
/// Request credentials from Creddy and output to stdout
|
||||
Get(GetArgs),
|
||||
/// Inject credentials into the environment of another command
|
||||
Exec(ExecArgs),
|
||||
/// Invoke an action normally triggered by hotkey (e.g. launch terminal)
|
||||
Shortcut(InvokeArgs),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct GetArgs {
|
||||
/// If unspecified, use default credentials
|
||||
#[arg(short, long)]
|
||||
name: Option<String>,
|
||||
/// Use base credentials instead of session credentials (only applicable to AWS)
|
||||
#[arg(long, short, default_value_t = false)]
|
||||
base: bool,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct ExecArgs {
|
||||
#[command(flatten)]
|
||||
get_args: GetArgs,
|
||||
#[arg(trailing_var_arg = true)]
|
||||
/// Command to be wrapped
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct InvokeArgs {
|
||||
#[arg(value_name = "ACTION", value_enum)]
|
||||
shortcut_action: ShortcutAction,
|
||||
}
|
||||
|
||||
|
||||
pub fn get(args: GetArgs, global: GlobalArgs) -> anyhow::Result<()> {
|
||||
let req = CliRequest::GetCredential {
|
||||
name: args.name,
|
||||
base: args.base,
|
||||
};
|
||||
|
||||
let output = match make_request(global.server_addr, &req)?? {
|
||||
CliResponse::Credential(CliCredential::AwsBase(c)) => {
|
||||
serde_json::to_string_pretty(&c).unwrap()
|
||||
},
|
||||
CliResponse::Credential(CliCredential::AwsSession(c)) => {
|
||||
serde_json::to_string_pretty(&c).unwrap()
|
||||
},
|
||||
r => bail!("Unexpected response from server: {r}"),
|
||||
};
|
||||
println!("{output}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn exec(args: ExecArgs, global: GlobalArgs) -> anyhow::Result<()> {
|
||||
// Clap guarantees that cmd_line will be a sequence of at least 1 item
|
||||
// test this!
|
||||
let mut cmd_line = args.command.iter();
|
||||
let cmd_name = cmd_line.next().unwrap();
|
||||
let mut cmd = ChildCommand::new(cmd_name);
|
||||
cmd.args(cmd_line);
|
||||
|
||||
let req = CliRequest::GetCredential {
|
||||
name: args.get_args.name,
|
||||
base: args.get_args.base,
|
||||
};
|
||||
|
||||
match make_request(global.server_addr, &req)?? {
|
||||
CliResponse::Credential(CliCredential::AwsBase(creds)) => {
|
||||
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
||||
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
||||
},
|
||||
CliResponse::Credential(CliCredential::AwsSession(creds)) => {
|
||||
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
||||
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
||||
cmd.env("AWS_SESSION_TOKEN", creds.session_token);
|
||||
},
|
||||
r => bail!("Unexpected response from server: {r}"),
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let e = cmd.exec();
|
||||
// cmd.exec() never returns if successful, so we never hit this line unless there's an error
|
||||
Err(e).with_context(|| {
|
||||
// eventually figure out how to display the actual command
|
||||
format!("Failed to execute command: {}", args.command.join(" "))
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut child = match cmd.spawn() {
|
||||
Ok(c) => c,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
let name: OsString = cmd_name.into();
|
||||
return Err(ExecError::NotFound(name).into());
|
||||
}
|
||||
Err(e) => return Err(ExecError::ExecutionFailed(e).into()),
|
||||
};
|
||||
|
||||
let status = child.wait()
|
||||
.map_err(|e| ExecError::ExecutionFailed(e))?;
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
pub fn invoke_shortcut(args: InvokeArgs, global: GlobalArgs) -> anyhow::Result<()> {
|
||||
let req = CliRequest::InvokeShortcut(args.shortcut_action);
|
||||
match make_request(global.server_addr, &req)?? {
|
||||
CliResponse::Empty => Ok(()),
|
||||
r => bail!("Unexpected response from server: {r}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Explanation for double-result: the server will return a (serialized) Result
|
||||
// to indicate when the operation succeeded or failed, which we deserialize.
|
||||
// However, the operation may fail to even communicate with the server, in
|
||||
// which case we return the outer Result
|
||||
#[tokio::main]
|
||||
async fn make_request(
|
||||
addr: Option<PathBuf>,
|
||||
req: &CliRequest
|
||||
) -> anyhow::Result<Result<CliResponse, ServerError>> {
|
||||
let mut data = serde_json::to_string(req).unwrap();
|
||||
// server expects newline marking end of request
|
||||
data.push('\n');
|
||||
|
||||
let mut stream = crate::connect(addr).await?;
|
||||
stream.write_all(&data.as_bytes()).await?;
|
||||
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
stream.read_to_end(&mut buf).await?;
|
||||
let res: Result<CliResponse, ServerError> = serde_json::from_slice(&buf)?;
|
||||
Ok(res)
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
mod cli;
|
||||
pub use cli::{
|
||||
Cli,
|
||||
Action,
|
||||
exec,
|
||||
get,
|
||||
invoke_shortcut,
|
||||
};
|
||||
|
||||
pub(crate) use platform::connect;
|
||||
pub use platform::server_addr;
|
||||
|
||||
pub mod proto;
|
||||
|
||||
|
||||
#[cfg(unix)]
|
||||
mod platform {
|
||||
use std::path::PathBuf;
|
||||
use tokio::net::UnixStream;
|
||||
|
||||
pub async fn connect(addr: Option<PathBuf>) -> Result<UnixStream, std::io::Error> {
|
||||
let path = addr.unwrap_or_else(|| server_addr("creddy-server"));
|
||||
UnixStream::connect(&path).await
|
||||
}
|
||||
|
||||
pub fn server_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 {
|
||||
pub fn server_addr(sock_name: &str) -> String {
|
||||
format!(r"\\.\pipe\{sock_name}")
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
use std::env;
|
||||
use std::process::{self, Command};
|
||||
|
||||
use creddy_cli::{Action, Cli};
|
||||
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
let res = match cli.action {
|
||||
None | Some(Action::Run)=> launch_gui(),
|
||||
Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args),
|
||||
Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args),
|
||||
Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
eprintln!("Error: {e:?}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn launch_gui() -> anyhow::Result<()> {
|
||||
let mut path = env::current_exe()?;
|
||||
path.pop(); // bin dir
|
||||
|
||||
// binaries are colocated in dev, but not in production
|
||||
#[cfg(not(debug_assertions))]
|
||||
path.pop(); // install dir
|
||||
|
||||
path.push("creddy.exe"); // exe in main install dir (aka gui exe)
|
||||
|
||||
Command::new(path).spawn()?;
|
||||
Ok(())
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
use std::fmt::{
|
||||
Display,
|
||||
Formatter,
|
||||
Error as FmtError
|
||||
};
|
||||
|
||||
use clap::ValueEnum;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliRequest {
|
||||
GetCredential {
|
||||
name: Option<String>,
|
||||
base: bool,
|
||||
},
|
||||
InvokeShortcut(ShortcutAction),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, ValueEnum)]
|
||||
pub enum ShortcutAction {
|
||||
ShowWindow,
|
||||
LaunchTerminal,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliResponse {
|
||||
Credential(CliCredential),
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl Display for CliResponse {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
|
||||
match self {
|
||||
CliResponse::Credential(CliCredential::AwsBase(_)) => write!(f, "Credential (AwsBase)"),
|
||||
CliResponse::Credential(CliCredential::AwsSession(_)) => write!(f, "Credential (AwsSession)"),
|
||||
CliResponse::Empty => write!(f, "Empty"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliCredential {
|
||||
AwsBase(AwsBaseCredential),
|
||||
AwsSession(AwsSessionCredential),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct AwsBaseCredential {
|
||||
#[serde(default = "default_aws_version")]
|
||||
pub version: usize,
|
||||
pub access_key_id: String,
|
||||
pub secret_access_key: String,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct AwsSessionCredential {
|
||||
#[serde(default = "default_aws_version")]
|
||||
pub version: usize,
|
||||
pub access_key_id: String,
|
||||
pub secret_access_key: String,
|
||||
pub session_token: String,
|
||||
// we don't need to know the expiration for the CLI, so just use a string here
|
||||
pub expiration: String,
|
||||
}
|
||||
|
||||
|
||||
fn default_aws_version() -> usize { 1 }
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ServerError {
|
||||
code: String,
|
||||
msg: String,
|
||||
}
|
||||
|
||||
impl Display for ServerError {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), FmtError> {
|
||||
write!(f, "Error response ({}) from server: {}", self.code, self.msg)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for ServerError {}
|
@ -21,7 +21,7 @@ use crate::{
|
||||
config::{self, AppConfig},
|
||||
credentials::AppSession,
|
||||
ipc,
|
||||
srv::{creddy_server, agent},
|
||||
server::{Server, Agent},
|
||||
errors::*,
|
||||
shortcuts,
|
||||
state::AppState,
|
||||
@ -53,7 +53,6 @@ pub fn run() -> tauri::Result<()> {
|
||||
ipc::delete_credential,
|
||||
ipc::list_credentials,
|
||||
ipc::sshkey_from_file,
|
||||
ipc::sshkey_from_private_key,
|
||||
ipc::get_config,
|
||||
ipc::save_config,
|
||||
ipc::launch_terminal,
|
||||
@ -106,8 +105,8 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> {
|
||||
};
|
||||
|
||||
let app_session = AppSession::load(&pool).await?;
|
||||
creddy_server::serve(app.handle().clone())?;
|
||||
agent::serve(app.handle().clone())?;
|
||||
Server::start(app.handle().clone())?;
|
||||
Agent::start(app.handle().clone())?;
|
||||
|
||||
config::set_auto_launch(conf.start_on_login)?;
|
||||
if let Err(_e) = config::set_auto_launch(conf.start_on_login) {
|
||||
|
47
src-tauri/src/bin/creddy_cli.rs
Normal file
47
src-tauri/src/bin/creddy_cli.rs
Normal file
@ -0,0 +1,47 @@
|
||||
// Windows isn't really amenable to having a single executable work as both a CLI and GUI app,
|
||||
// so we just have a second binary for CLI usage
|
||||
use creddy::{
|
||||
cli,
|
||||
errors::CliError,
|
||||
};
|
||||
use std::{
|
||||
env,
|
||||
process::{self, Command},
|
||||
};
|
||||
|
||||
|
||||
fn main() {
|
||||
let args = cli::parser().get_matches();
|
||||
if let Some(true) = args.get_one::<bool>("help") {
|
||||
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(),
|
||||
Some(("get", m)) => cli::get(m),
|
||||
Some(("exec", m)) => cli::exec(m),
|
||||
Some(("shortcut", m)) => cli::invoke_shortcut(m),
|
||||
_ => unreachable!("Unknown subcommand"),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
eprintln!("Error: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn launch_gui() -> Result<(), CliError> {
|
||||
let mut path = env::current_exe()?;
|
||||
path.pop(); // bin dir
|
||||
|
||||
// binaries are colocated in dev, but not in production
|
||||
#[cfg(not(debug_assertions))]
|
||||
path.pop(); // install dir
|
||||
|
||||
path.push("creddy.exe"); // exe in main install dir (aka gui exe)
|
||||
|
||||
Command::new(path).spawn()?;
|
||||
Ok(())
|
||||
}
|
194
src-tauri/src/cli.rs
Normal file
194
src-tauri/src/cli.rs
Normal file
@ -0,0 +1,194 @@
|
||||
use std::ffi::OsString;
|
||||
use std::process::Command as ChildCommand;
|
||||
#[cfg(windows)]
|
||||
use std::time::Duration;
|
||||
|
||||
use clap::{
|
||||
Command,
|
||||
Arg,
|
||||
ArgMatches,
|
||||
ArgAction,
|
||||
builder::PossibleValuesParser,
|
||||
};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::server::{Request, Response};
|
||||
use crate::shortcuts::ShortcutAction;
|
||||
|
||||
#[cfg(unix)]
|
||||
use {
|
||||
std::os::unix::process::CommandExt,
|
||||
tokio::net::UnixStream,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
use {
|
||||
tokio::net::windows::named_pipe::{NamedPipeClient, ClientOptions},
|
||||
windows::Win32::Foundation::ERROR_PIPE_BUSY,
|
||||
};
|
||||
|
||||
|
||||
pub fn parser() -> Command<'static> {
|
||||
Command::new("creddy")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about("A friendly AWS credentials manager")
|
||||
.subcommand(
|
||||
Command::new("run")
|
||||
.about("Launch Creddy")
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("get")
|
||||
.about("Request AWS credentials from Creddy and output to stdout")
|
||||
.arg(
|
||||
Arg::new("base")
|
||||
.short('b')
|
||||
.long("base")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Use base credentials instead of session credentials")
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("exec")
|
||||
.about("Inject AWS credentials into the environment of another command")
|
||||
.trailing_var_arg(true)
|
||||
.arg(
|
||||
Arg::new("base")
|
||||
.short('b')
|
||||
.long("base")
|
||||
.action(ArgAction::SetTrue)
|
||||
.help("Use base credentials instead of session credentials")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("command")
|
||||
.multiple_values(true)
|
||||
)
|
||||
)
|
||||
.subcommand(
|
||||
Command::new("shortcut")
|
||||
.about("Invoke an action normally trigged by hotkey (e.g. launch terminal)")
|
||||
.arg(
|
||||
Arg::new("action")
|
||||
.value_parser(
|
||||
PossibleValuesParser::new(["show_window", "launch_terminal"])
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
pub fn get(args: &ArgMatches) -> Result<(), CliError> {
|
||||
let base = args.get_one("base").unwrap_or(&false);
|
||||
let output = match make_request(&Request::GetAwsCredentials { base: *base })? {
|
||||
Response::AwsBase(creds) => serde_json::to_string(&creds).unwrap(),
|
||||
Response::AwsSession(creds) => serde_json::to_string(&creds).unwrap(),
|
||||
r => return Err(RequestError::Unexpected(r).into()),
|
||||
};
|
||||
println!("{output}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn exec(args: &ArgMatches) -> Result<(), CliError> {
|
||||
let base = *args.get_one("base").unwrap_or(&false);
|
||||
let mut cmd_line = args.get_many("command")
|
||||
.ok_or(ExecError::NoCommand)?;
|
||||
|
||||
let cmd_name: &String = cmd_line.next().unwrap(); // Clap guarantees that there will be at least one
|
||||
let mut cmd = ChildCommand::new(cmd_name);
|
||||
cmd.args(cmd_line);
|
||||
|
||||
match make_request(&Request::GetAwsCredentials { base })? {
|
||||
Response::AwsBase(creds) => {
|
||||
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
||||
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
||||
},
|
||||
Response::AwsSession(creds) => {
|
||||
cmd.env("AWS_ACCESS_KEY_ID", creds.access_key_id);
|
||||
cmd.env("AWS_SECRET_ACCESS_KEY", creds.secret_access_key);
|
||||
cmd.env("AWS_SESSION_TOKEN", creds.session_token);
|
||||
},
|
||||
r => return Err(RequestError::Unexpected(r).into()),
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
// cmd.exec() never returns if successful
|
||||
let e = cmd.exec();
|
||||
match e.kind() {
|
||||
std::io::ErrorKind::NotFound => {
|
||||
let name: OsString = cmd_name.into();
|
||||
Err(ExecError::NotFound(name).into())
|
||||
}
|
||||
_ => Err(ExecError::ExecutionFailed(e).into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let mut child = match cmd.spawn() {
|
||||
Ok(c) => c,
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||
let name: OsString = cmd_name.into();
|
||||
return Err(ExecError::NotFound(name).into());
|
||||
}
|
||||
Err(e) => return Err(ExecError::ExecutionFailed(e).into()),
|
||||
};
|
||||
|
||||
let status = child.wait()
|
||||
.map_err(|e| ExecError::ExecutionFailed(e))?;
|
||||
std::process::exit(status.code().unwrap_or(1));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
pub fn invoke_shortcut(args: &ArgMatches) -> Result<(), CliError> {
|
||||
let action = match args.get_one::<String>("action").map(|s| s.as_str()) {
|
||||
Some("show_window") => ShortcutAction::ShowWindow,
|
||||
Some("launch_terminal") => ShortcutAction::LaunchTerminal,
|
||||
Some(&_) | None => unreachable!("Unknown shortcut action"), // guaranteed by clap
|
||||
};
|
||||
|
||||
let req = Request::InvokeShortcut(action);
|
||||
match make_request(&req) {
|
||||
Ok(Response::Empty) => Ok(()),
|
||||
Ok(r) => Err(RequestError::Unexpected(r).into()),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn make_request(req: &Request) -> Result<Response, RequestError> {
|
||||
let mut data = serde_json::to_string(req).unwrap();
|
||||
// server expects newline marking end of request
|
||||
data.push('\n');
|
||||
|
||||
let mut stream = connect().await?;
|
||||
stream.write_all(&data.as_bytes()).await?;
|
||||
|
||||
let mut buf = Vec::with_capacity(1024);
|
||||
stream.read_to_end(&mut buf).await?;
|
||||
let res: Result<Response, ServerError> = serde_json::from_slice(&buf)?;
|
||||
Ok(res?)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn connect() -> Result<NamedPipeClient, std::io::Error> {
|
||||
// apparently attempting to connect can fail if there's already a client connected
|
||||
loop {
|
||||
match ClientOptions::new().open(r"\\.\pipe\creddy-requests") {
|
||||
Ok(stream) => return Ok(stream),
|
||||
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => (),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn connect() -> Result<UnixStream, std::io::Error> {
|
||||
UnixStream::connect("/tmp/creddy.sock").await
|
||||
}
|
@ -76,7 +76,7 @@ impl PersistentCredential for AwsBaseCredential {
|
||||
access_key_id,
|
||||
secret_key_enc,
|
||||
nonce
|
||||
)
|
||||
)
|
||||
VALUES (?, ?, ?, ?);",
|
||||
id, self.access_key_id, ciphertext, nonce_bytes,
|
||||
).execute(&mut **txn).await?;
|
||||
@ -185,16 +185,10 @@ where S: Serializer
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use aws_sdk_sts::primitives::DateTimeFormat;
|
||||
use creddy_cli::proto::{
|
||||
AwsBaseCredential as CliBase,
|
||||
AwsSessionCredential as CliSession,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::types::uuid::uuid;
|
||||
|
||||
|
||||
|
||||
fn creds() -> AwsBaseCredential {
|
||||
AwsBaseCredential::new(
|
||||
"AKIAIOSFODNN7EXAMPLE".into(),
|
||||
@ -209,6 +203,19 @@ 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"))]
|
||||
async fn test_load(pool: SqlitePool) {
|
||||
@ -247,99 +254,5 @@ mod tests {
|
||||
|
||||
assert_eq!(&creds().into_credential(), &list[0]);
|
||||
assert_eq!(&creds_2().into_credential(), &list[1]);
|
||||
}
|
||||
|
||||
|
||||
// In order to avoid the CLI depending on the main app (and thus defeating the purpose
|
||||
// of having a separate CLI at all) it re-defines the credentials that need to be sent
|
||||
// back and forth. To prevent the separate definitions from drifting aprt, we test
|
||||
// serializing/deserializing in both directions.
|
||||
|
||||
#[test]
|
||||
fn test_cli_to_app_base() {
|
||||
let cli_base = CliBase {
|
||||
version: 1,
|
||||
access_key_id: "AKIAIOSFODNN7EXAMPLE".into(),
|
||||
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&cli_base).unwrap();
|
||||
let computed: AwsBaseCredential = serde_json::from_str(&json)
|
||||
.expect("Failed to deserialize base credentials from CLI -> main app");
|
||||
|
||||
assert_eq!(creds(), computed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_app_to_cli_base() {
|
||||
let base = creds();
|
||||
let json = serde_json::to_string(&base).unwrap();
|
||||
|
||||
let computed: CliBase = serde_json::from_str(&json)
|
||||
.expect("Failed to deserialize base credentials from main app -> CLI");
|
||||
|
||||
let expected = CliBase {
|
||||
version: 1,
|
||||
access_key_id: "AKIAIOSFODNN7EXAMPLE".into(),
|
||||
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, computed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_to_app_session() {
|
||||
let cli_session = CliSession {
|
||||
version: 1,
|
||||
access_key_id: "ASIAIOSFODNN7EXAMPLE".into(),
|
||||
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
|
||||
session_token: "JQ70sxbqnOGKu7+krevstYCLCaX2+alUAT60ARTBBnQ=ETC.".into(),
|
||||
expiration: "2024-07-21T00:00:00Z".into(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&cli_session).unwrap();
|
||||
let computed: AwsSessionCredential = serde_json::from_str(&json)
|
||||
.expect("Failed to deserialize session credentials from CLI -> main app");
|
||||
|
||||
let expected = AwsSessionCredential {
|
||||
version: 1,
|
||||
access_key_id: "ASIAIOSFODNN7EXAMPLE".into(),
|
||||
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
|
||||
session_token: "JQ70sxbqnOGKu7+krevstYCLCaX2+alUAT60ARTBBnQ=ETC.".into(),
|
||||
expiration: DateTime::from_str(
|
||||
"2024-07-21T00:00:00Z",
|
||||
DateTimeFormat::DateTimeWithOffset
|
||||
).unwrap(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, computed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_app_to_cli_session() {
|
||||
let session = AwsSessionCredential {
|
||||
version: 1,
|
||||
access_key_id: "ASIAIOSFODNN7EXAMPLE".into(),
|
||||
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
|
||||
session_token: "JQ70sxbqnOGKu7+krevstYCLCaX2+alUAT60ARTBBnQ=ETC.".into(),
|
||||
expiration: DateTime::from_str(
|
||||
"2024-07-21T00:00:00Z",
|
||||
DateTimeFormat::DateTimeWithOffset
|
||||
).unwrap(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&session).unwrap();
|
||||
let computed: CliSession = serde_json::from_str(&json)
|
||||
.expect("Failed to deserialize session credentials from main app -> CLI");
|
||||
|
||||
let expected = CliSession {
|
||||
version: 1,
|
||||
access_key_id: "ASIAIOSFODNN7EXAMPLE".into(),
|
||||
secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".into(),
|
||||
session_token: "JQ70sxbqnOGKu7+krevstYCLCaX2+alUAT60ARTBBnQ=ETC.".into(),
|
||||
expiration: "2024-07-21T00:00:00Z".into(),
|
||||
};
|
||||
|
||||
assert_eq!(expected, computed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,3 @@
|
||||
INSERT INTO credentials (id, name, credential_type, is_default, created_at)
|
||||
VALUES
|
||||
(X'11111111111111111111111111111111', 'ssh-plain', 'ssh', 1, 1721557273),
|
||||
(X'22222222222222222222222222222222', 'ssh-enc', 'ssh', 0, 1721557274),
|
||||
(X'33333333333333333333333333333333', 'ed25519-plain', 'ssh', 0, 1721557275),
|
||||
(X'44444444444444444444444444444444', 'ed25519-enc', 'ssh', 0, 1721557276);
|
||||
|
||||
|
||||
INSERT INTO ssh_credentials (id, algorithm, comment, public_key, private_key_enc, nonce)
|
||||
VALUES
|
||||
(
|
||||
|
@ -112,30 +112,19 @@ impl CredentialRecord {
|
||||
Ok(Self::from_parts(row, credential))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE id = ?")
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
.ok_or(LoadCredentialsError::NoCredentials)?;
|
||||
// pub async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
// let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE id = ?")
|
||||
// .bind(id)
|
||||
// .fetch_optional(pool)
|
||||
// .await?
|
||||
// .ok_or(LoadCredentialsError::NoCredentials)?;
|
||||
|
||||
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
|
||||
}
|
||||
// Self::load_credential(row, crypto, pool).await
|
||||
// }
|
||||
|
||||
pub async fn load_default(credential_type: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
let row: CredentialRow = sqlx::query_as(
|
||||
"SELECT * FROM credentials
|
||||
"SELECT * FROM credentials
|
||||
WHERE credential_type = ? AND is_default = 1"
|
||||
).bind(credential_type)
|
||||
.fetch_optional(pool)
|
||||
@ -420,7 +409,7 @@ mod uuid_tests {
|
||||
#[test]
|
||||
fn test_serialize_deserialize_uuid() {
|
||||
let buf = Crypto::salt();
|
||||
let expected = UuidWrapper{
|
||||
let expected = UuidWrapper{
|
||||
id: Uuid::from_slice(&buf[..16]).unwrap()
|
||||
};
|
||||
let serialized = serde_json::to_string(&expected).unwrap();
|
||||
|
@ -12,8 +12,6 @@ use serde::ser::{
|
||||
SerializeStruct,
|
||||
};
|
||||
use serde::de::{self, Visitor};
|
||||
use sha2::{Sha256, Sha512};
|
||||
use signature::{Signer, SignatureEncoding};
|
||||
use sqlx::{
|
||||
FromRow,
|
||||
Sqlite,
|
||||
@ -21,15 +19,11 @@ use sqlx::{
|
||||
Transaction,
|
||||
types::Uuid,
|
||||
};
|
||||
use ssh_agent_lib::proto::message::{
|
||||
Identity,
|
||||
SignRequest,
|
||||
};
|
||||
use ssh_encoding::Encode;
|
||||
use ssh_agent_lib::proto::message::Identity;
|
||||
use ssh_key::{
|
||||
Algorithm,
|
||||
LineEnding,
|
||||
private::{PrivateKey, KeypairData},
|
||||
private::PrivateKey,
|
||||
public::PublicKey,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
@ -80,26 +74,11 @@ 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> {
|
||||
let row = sqlx::query!(
|
||||
"SELECT c.name
|
||||
FROM credentials c
|
||||
JOIN ssh_credentials s
|
||||
JOIN ssh_credentials s
|
||||
ON s.id = c.id
|
||||
WHERE s.public_key = ?",
|
||||
pubkey
|
||||
@ -125,33 +104,6 @@ impl SshKey {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -168,7 +120,7 @@ impl PersistentCredential for SshKey {
|
||||
let nonce = XNonce::clone_from_slice(&row.nonce);
|
||||
let privkey_bytes = crypto.decrypt(&nonce, &row.private_key_enc)?;
|
||||
|
||||
|
||||
|
||||
let algorithm = Algorithm::new(&row.algorithm)
|
||||
.map_err(|_| LoadCredentialsError::InvalidData)?;
|
||||
let public_key = PublicKey::from_bytes(&row.public_key)
|
||||
@ -298,9 +250,8 @@ fn deserialize_algorithm<'de, D>(deserializer: D) -> Result<Algorithm, D::Error>
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs::{self, File};
|
||||
use ssh_key::Fingerprint;
|
||||
use sqlx::types::uuid::uuid;
|
||||
use crate::credentials::CredentialRecord;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn path(name: &str) -> String {
|
||||
@ -342,7 +293,7 @@ mod tests {
|
||||
let k = rsa_plain();
|
||||
assert_eq!(k.algorithm.as_str(), "ssh-rsa");
|
||||
assert_eq!(&k.comment, "hello world");
|
||||
|
||||
|
||||
assert_eq!(
|
||||
k.public_key.fingerprint(Default::default()),
|
||||
k.private_key.fingerprint(Default::default()),
|
||||
@ -360,7 +311,7 @@ mod tests {
|
||||
let k = rsa_enc();
|
||||
assert_eq!(k.algorithm.as_str(), "ssh-rsa");
|
||||
assert_eq!(&k.comment, "hello world");
|
||||
|
||||
|
||||
assert_eq!(
|
||||
k.public_key.fingerprint(Default::default()),
|
||||
k.private_key.fingerprint(Default::default()),
|
||||
@ -378,7 +329,7 @@ mod tests {
|
||||
let k = ed25519_plain();
|
||||
assert_eq!(k.algorithm.as_str(),"ssh-ed25519");
|
||||
assert_eq!(&k.comment, "hello world");
|
||||
|
||||
|
||||
assert_eq!(
|
||||
k.public_key.fingerprint(Default::default()),
|
||||
k.private_key.fingerprint(Default::default()),
|
||||
@ -396,7 +347,7 @@ mod tests {
|
||||
let k = ed25519_enc();
|
||||
assert_eq!(k.algorithm.as_str(), "ssh-ed25519");
|
||||
assert_eq!(&k.comment, "hello world");
|
||||
|
||||
|
||||
assert_eq!(
|
||||
k.public_key.fingerprint(Default::default()),
|
||||
k.private_key.fingerprint(Default::default()),
|
||||
@ -436,14 +387,11 @@ mod tests {
|
||||
#[sqlx::test]
|
||||
async fn test_save_db(pool: SqlitePool) {
|
||||
let crypto = Crypto::random();
|
||||
let record = CredentialRecord {
|
||||
id: random_uuid(),
|
||||
name: "save_test".into(),
|
||||
is_default: false,
|
||||
credential: Credential::Ssh(rsa_plain()),
|
||||
};
|
||||
record.save(&crypto, &pool).await
|
||||
.expect("Failed to save SSH key CredentialRecord to database");
|
||||
let k = rsa_plain();
|
||||
let mut txn = pool.begin().await.unwrap();
|
||||
k.save_details(&random_uuid(), &crypto, &mut txn).await
|
||||
.expect("Failed to save SSH key to database");
|
||||
txn.commit().await.expect("Failed to finalize transaction");
|
||||
}
|
||||
|
||||
|
||||
@ -451,7 +399,7 @@ mod tests {
|
||||
async fn test_load_db(pool: SqlitePool) {
|
||||
let crypto = Crypto::fixed();
|
||||
let id = uuid!("11111111-1111-1111-1111-111111111111");
|
||||
SshKey::load(&id, &crypto, &pool).await
|
||||
let k = SshKey::load(&id, &crypto, &pool).await
|
||||
.expect("Failed to load SSH key from database");
|
||||
}
|
||||
|
||||
@ -459,18 +407,13 @@ mod tests {
|
||||
#[sqlx::test]
|
||||
async fn test_save_load_db(pool: SqlitePool) {
|
||||
let crypto = Crypto::random();
|
||||
|
||||
let id = random_uuid();
|
||||
let record = CredentialRecord {
|
||||
id,
|
||||
name: "save_load_test".into(),
|
||||
is_default: false,
|
||||
credential: Credential::Ssh(ed25519_plain()),
|
||||
};
|
||||
|
||||
record.save(&crypto, &pool).await.unwrap();
|
||||
let loaded = SshKey::load(&id, &crypto, &pool).await.unwrap();
|
||||
let id = uuid!("7bc994dd-113a-4841-bcf7-b47c2fffdd25");
|
||||
let known = ed25519_plain();
|
||||
let mut txn = pool.begin().await.unwrap();
|
||||
known.save_details(&id, &crypto, &mut txn).await.unwrap();
|
||||
txn.commit().await.unwrap();
|
||||
|
||||
let loaded = SshKey::load(&id, &crypto, &pool).await.unwrap();
|
||||
|
||||
assert_eq!(known.algorithm, loaded.algorithm);
|
||||
assert_eq!(known.comment, loaded.comment);
|
||||
|
@ -36,7 +36,7 @@ pub trait ShowError<T, E>
|
||||
fn error_print_prefix(self, prefix: &str);
|
||||
}
|
||||
|
||||
impl<T, E> ShowError<T, E> for Result<T, E>
|
||||
impl<T, E> ShowError<T, E> for Result<T, E>
|
||||
where E: std::fmt::Display
|
||||
{
|
||||
fn error_popup(self, title: &str) {
|
||||
@ -91,7 +91,7 @@ impl<E: Error> Serialize for SerializeUpstream<E> {
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_upstream_err<E, M>(err: &E, map: &mut M) -> Result<(), M::Error>
|
||||
fn serialize_upstream_err<E, M>(err: &E, map: &mut M) -> Result<(), M::Error>
|
||||
where
|
||||
E: Error,
|
||||
M: serde::ser::SerializeMap,
|
||||
@ -195,10 +195,6 @@ pub enum HandlerError {
|
||||
SshAgent(#[from] ssh_agent_lib::error::AgentError),
|
||||
#[error(transparent)]
|
||||
SshKey(#[from] ssh_key::Error),
|
||||
#[error(transparent)]
|
||||
Signature(#[from] signature::Error),
|
||||
#[error(transparent)]
|
||||
Encoding(#[from] ssh_encoding::Error),
|
||||
}
|
||||
|
||||
|
||||
@ -342,8 +338,6 @@ pub enum ClientInfoError {
|
||||
#[cfg(windows)]
|
||||
#[error("Could not determine PID of connected client")]
|
||||
WindowsError(#[from] windows::core::Error),
|
||||
#[error("Could not determine PID of connected client")]
|
||||
PidNotFound,
|
||||
#[error(transparent)]
|
||||
Io(#[from] std::io::Error),
|
||||
}
|
||||
@ -370,7 +364,7 @@ pub enum RequestError {
|
||||
#[error("Error response from server: {0}")]
|
||||
Server(ServerError),
|
||||
#[error("Unexpected response from server")]
|
||||
Unexpected(crate::srv::CliResponse),
|
||||
Unexpected(crate::server::Response),
|
||||
#[error("The server did not respond with valid JSON")]
|
||||
InvalidJson(#[from] serde_json::Error),
|
||||
#[error("Error reading/writing stream: {0}")]
|
||||
|
@ -18,7 +18,6 @@ use crate::terminal;
|
||||
pub struct AwsRequestNotification {
|
||||
pub id: u64,
|
||||
pub client: Client,
|
||||
pub name: Option<String>,
|
||||
pub base: bool,
|
||||
}
|
||||
|
||||
@ -39,8 +38,8 @@ pub enum RequestNotification {
|
||||
}
|
||||
|
||||
impl RequestNotification {
|
||||
pub fn new_aws(id: u64, client: Client, name: Option<String>, base: bool) -> Self {
|
||||
Self::Aws(AwsRequestNotification {id, client, name, base})
|
||||
pub fn new_aws(id: u64, client: Client, base: bool) -> Self {
|
||||
Self::Aws(AwsRequestNotification {id, client, base})
|
||||
}
|
||||
|
||||
pub fn new_ssh(id: u64, client: Client, key_name: String) -> Self {
|
||||
@ -142,12 +141,6 @@ 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]
|
||||
pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> {
|
||||
let config = app_state.config.read().await;
|
||||
|
@ -44,23 +44,21 @@ pub async fn load_bytes(pool: &SqlitePool, name: &str) -> Result<Option<Vec<u8>>
|
||||
}
|
||||
|
||||
|
||||
// we don't have a need for this right now, but we will some day
|
||||
#[cfg(test)]
|
||||
pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM kv WHERE name = ?", name)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
// pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
|
||||
// sqlx::query!("DELETE FROM kv WHERE name = ?", name)
|
||||
// .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()
|
||||
.map(|_| "?")
|
||||
.collect::<Vec<&str>>()
|
||||
.join(",");
|
||||
let query = format!("DELETE FROM kv WHERE name IN ({})", placeholder);
|
||||
|
||||
|
||||
let mut q = sqlx::query(&query);
|
||||
for name in names {
|
||||
q = q.bind(name);
|
||||
@ -85,7 +83,7 @@ macro_rules! load_bytes_multi {
|
||||
(
|
||||
// ...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
|
||||
match crate::kv::load_bytes($pool, $name).await? {
|
||||
Some(v) => v,
|
||||
@ -189,7 +187,7 @@ mod tests {
|
||||
async fn test_delete(pool: SqlitePool) {
|
||||
delete(&pool, "test_bytes").await
|
||||
.expect("Failed to delete data");
|
||||
|
||||
|
||||
let loaded = load_bytes(&pool, "test_bytes").await
|
||||
.expect("Failed to load data");
|
||||
assert_eq!(loaded, None);
|
||||
|
@ -1,4 +1,5 @@
|
||||
pub mod app;
|
||||
pub mod cli;
|
||||
mod config;
|
||||
mod credentials;
|
||||
pub mod errors;
|
||||
@ -6,7 +7,7 @@ mod clientinfo;
|
||||
mod ipc;
|
||||
mod kv;
|
||||
mod state;
|
||||
mod srv;
|
||||
pub mod server;
|
||||
mod shortcuts;
|
||||
mod terminal;
|
||||
mod tray;
|
||||
|
@ -3,24 +3,23 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
|
||||
use creddy::{
|
||||
app,
|
||||
cli,
|
||||
errors::ShowError,
|
||||
};
|
||||
use creddy_cli::{Action, Cli};
|
||||
|
||||
|
||||
fn main() {
|
||||
let cli = Cli::parse();
|
||||
let res = match cli.action {
|
||||
None | Some(Action::Run) => {
|
||||
let res = match cli::parser().get_matches().subcommand() {
|
||||
None | Some(("run", _)) => {
|
||||
app::run().error_popup("Creddy encountered an error");
|
||||
Ok(())
|
||||
},
|
||||
Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args),
|
||||
Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args),
|
||||
Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args),
|
||||
Some(("get", m)) => cli::get(m),
|
||||
Some(("exec", m)) => cli::exec(m),
|
||||
Some(("shortcut", m)) => cli::invoke_shortcut(m),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
|
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();
|
||||
}
|
@ -1,31 +1,75 @@
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::clientinfo::{self, Client};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::ipc::{Approval, RequestNotification};
|
||||
use crate::shortcuts::{self, ShortcutAction};
|
||||
use crate::state::AppState;
|
||||
use super::{
|
||||
CloseWaiter,
|
||||
CliCredential,
|
||||
CliRequest,
|
||||
CliResponse,
|
||||
Stream,
|
||||
use crate::clientinfo::{self, Client};
|
||||
use crate::credentials::{
|
||||
AwsBaseCredential,
|
||||
AwsSessionCredential,
|
||||
};
|
||||
use crate::ipc::{Approval, RequestNotification};
|
||||
use crate::state::AppState;
|
||||
use crate::shortcuts::{self, ShortcutAction};
|
||||
|
||||
#[cfg(windows)]
|
||||
mod server_win;
|
||||
#[cfg(windows)]
|
||||
pub use server_win::Server;
|
||||
#[cfg(windows)]
|
||||
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;
|
||||
|
||||
|
||||
pub fn serve(app_handle: AppHandle) -> std::io::Result<()> {
|
||||
super::serve("creddy-server", app_handle, handle)
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum Request {
|
||||
GetAwsCredentials{
|
||||
base: bool,
|
||||
},
|
||||
InvokeShortcut(ShortcutAction),
|
||||
}
|
||||
|
||||
|
||||
async fn handle(
|
||||
mut stream: Stream,
|
||||
app_handle: AppHandle,
|
||||
client_pid: u32
|
||||
) -> Result<(), HandlerError> {
|
||||
#[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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async fn handle(mut stream: Stream, app_handle: AppHandle, client_pid: u32) -> Result<(), HandlerError>
|
||||
{
|
||||
// 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 n = 0;
|
||||
@ -34,8 +78,7 @@ async fn handle(
|
||||
if let Some(&b'\n') = buf.last() {
|
||||
break;
|
||||
}
|
||||
// sanity check, no request should ever be within a mile of 1MB
|
||||
else if n >= (1024 * 1024) {
|
||||
else if n >= 1024 {
|
||||
return Err(HandlerError::RequestTooLarge);
|
||||
}
|
||||
}
|
||||
@ -43,13 +86,12 @@ async fn handle(
|
||||
let client = clientinfo::get_client(client_pid, true)?;
|
||||
let waiter = CloseWaiter { stream: &mut stream };
|
||||
|
||||
|
||||
let req: CliRequest = serde_json::from_slice(&buf)?;
|
||||
let req: Request = serde_json::from_slice(&buf)?;
|
||||
let res = match req {
|
||||
CliRequest::GetCredential{ name, base } => get_aws_credentials(
|
||||
name, base, client, app_handle, waiter
|
||||
Request::GetAwsCredentials{ base } => get_aws_credentials(
|
||||
base, client, app_handle, waiter
|
||||
).await,
|
||||
CliRequest::InvokeShortcut(action) => invoke_shortcut(action).await,
|
||||
Request::InvokeShortcut(action) => invoke_shortcut(action).await,
|
||||
};
|
||||
|
||||
// doesn't make sense to send the error to the client if the client has already left
|
||||
@ -63,19 +105,18 @@ async fn handle(
|
||||
}
|
||||
|
||||
|
||||
async fn invoke_shortcut(action: ShortcutAction) -> Result<CliResponse, HandlerError> {
|
||||
async fn invoke_shortcut(action: ShortcutAction) -> Result<Response, HandlerError> {
|
||||
shortcuts::exec_shortcut(action);
|
||||
Ok(CliResponse::Empty)
|
||||
Ok(Response::Empty)
|
||||
}
|
||||
|
||||
|
||||
async fn get_aws_credentials(
|
||||
name: Option<String>,
|
||||
base: bool,
|
||||
client: Client,
|
||||
app_handle: AppHandle,
|
||||
mut waiter: CloseWaiter<'_>,
|
||||
) -> Result<CliResponse, HandlerError> {
|
||||
) -> Result<Response, HandlerError> {
|
||||
let state = app_handle.state::<AppState>();
|
||||
let rehide_ms = {
|
||||
let config = state.config.read().await;
|
||||
@ -91,9 +132,7 @@ async fn get_aws_credentials(
|
||||
// but ? returns immediately, and we want to unregister the request before returning
|
||||
// so we bundle it all up in an async block and return a Result so we can handle errors
|
||||
let proceed = async {
|
||||
let notification = RequestNotification::new_aws(
|
||||
request_id, client, name.clone(), base
|
||||
);
|
||||
let notification = RequestNotification::new_aws(request_id, client, base);
|
||||
app_handle.emit("credential-request", ¬ification)?;
|
||||
|
||||
let response = tokio::select! {
|
||||
@ -107,12 +146,12 @@ async fn get_aws_credentials(
|
||||
match response.approval {
|
||||
Approval::Approved => {
|
||||
if response.base {
|
||||
let creds = state.get_aws_base(name).await?;
|
||||
Ok(CliResponse::Credential(CliCredential::AwsBase(creds)))
|
||||
let creds = state.get_aws_default().await?;
|
||||
Ok(Response::AwsBase(creds))
|
||||
}
|
||||
else {
|
||||
let creds = state.get_aws_session(name).await?.clone();
|
||||
Ok(CliResponse::Credential(CliCredential::AwsSession(creds)))
|
||||
let creds = state.get_aws_default_session().await?;
|
||||
Ok(Response::AwsSession(creds.clone()))
|
||||
}
|
||||
},
|
||||
Approval::Denied => Err(HandlerError::Denied),
|
||||
@ -124,7 +163,7 @@ async fn get_aws_credentials(
|
||||
Err(e) => {
|
||||
state.unregister_request(request_id).await;
|
||||
Err(e)
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
lease.release();
|
58
src-tauri/src/server/server_unix.rs
Normal file
58
src-tauri/src/server/server_unix.rs
Normal file
@ -0,0 +1,58 @@
|
||||
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)
|
||||
}
|
74
src-tauri/src/server/server_win.rs
Normal file
74
src-tauri/src/server/server_win.rs
Normal file
@ -0,0 +1,74 @@
|
||||
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)
|
||||
}
|
151
src-tauri/src/server/ssh_agent.rs
Normal file
151
src-tauri/src/server/ssh_agent.rs
Normal file
@ -0,0 +1,151 @@
|
||||
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);
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
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,164 +0,0 @@
|
||||
use std::future::Future;
|
||||
|
||||
use tauri::{
|
||||
AppHandle,
|
||||
async_runtime as rt,
|
||||
};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
use crate::credentials::{AwsBaseCredential, AwsSessionCredential};
|
||||
use crate::errors::*;
|
||||
use crate::shortcuts::ShortcutAction;
|
||||
|
||||
pub mod creddy_server;
|
||||
pub mod agent;
|
||||
use platform::Stream;
|
||||
|
||||
|
||||
// These types match what's defined in creddy_cli, but they are separate types
|
||||
// so that we avoid polluting the standalone CLI with a bunch of dependencies
|
||||
// that would make it impossible to build a completely static-linked version
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliRequest {
|
||||
GetCredential {
|
||||
name: Option<String>,
|
||||
base: bool,
|
||||
},
|
||||
InvokeShortcut(ShortcutAction),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliResponse {
|
||||
Credential(CliCredential),
|
||||
Empty,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliCredential {
|
||||
AwsBase(AwsBaseCredential),
|
||||
AwsSession(AwsSessionCredential),
|
||||
}
|
||||
|
||||
|
||||
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 = creddy_cli::server_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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[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 = creddy_cli::server_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))
|
||||
}
|
||||
}
|
@ -270,23 +270,22 @@ impl AppState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_aws_base(&self, name: Option<String>) -> Result<AwsBaseCredential, GetCredentialsError> {
|
||||
pub async fn get_aws_default(&self) -> Result<AwsBaseCredential, GetCredentialsError> {
|
||||
let app_session = self.app_session.read().await;
|
||||
let crypto = app_session.try_get_crypto()?;
|
||||
let creds = match name {
|
||||
Some(n) => AwsBaseCredential::load_by_name(&n, crypto, &self.pool).await?,
|
||||
None => AwsBaseCredential::load_default(crypto, &self.pool).await?,
|
||||
};
|
||||
let creds = AwsBaseCredential::load_default(crypto, &self.pool).await?;
|
||||
// let record = CredentialRecord::load_default("aws", crypto, &self.pool).await?;
|
||||
// let creds = match record.credential {
|
||||
// Credential::AwsBase(b) => Ok(b),
|
||||
// _ => Err(LoadCredentialsError::NoCredentials)
|
||||
// }?;
|
||||
Ok(creds)
|
||||
}
|
||||
|
||||
pub async fn get_aws_session(&self, name: Option<String>) -> Result<RwLockReadGuard<'_, AwsSessionCredential>, GetCredentialsError> {
|
||||
pub async fn get_aws_default_session(&self) -> Result<RwLockReadGuard<'_, AwsSessionCredential>, GetCredentialsError> {
|
||||
let app_session = self.app_session.read().await;
|
||||
let crypto = app_session.try_get_crypto()?;
|
||||
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 record = CredentialRecord::load_default("aws", crypto, &self.pool).await?;
|
||||
let base = match &record.credential {
|
||||
Credential::AwsBase(b) => Ok(b),
|
||||
_ => 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
|
||||
// this will result in an error popup to the user (see main hotkey handler)
|
||||
if use_base {
|
||||
let base_creds = state.get_aws_base(None).await?;
|
||||
let base_creds = state.get_aws_default().await?;
|
||||
cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id);
|
||||
cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key);
|
||||
}
|
||||
else {
|
||||
let session_creds = state.get_aws_session(None).await?;
|
||||
let session_creds = state.get_aws_default_session().await?;
|
||||
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_SESSION_TOKEN", &session_creds.session_token);
|
||||
|
@ -50,7 +50,7 @@
|
||||
}
|
||||
},
|
||||
"productName": "creddy",
|
||||
"version": "0.5.4",
|
||||
"version": "0.4.9",
|
||||
"identifier": "creddy",
|
||||
"plugins": {},
|
||||
"app": {
|
||||
@ -85,4 +85,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,11 +8,6 @@
|
||||
export {classes as class};
|
||||
|
||||
let show = false;
|
||||
let input;
|
||||
|
||||
export function focus() {
|
||||
input.focus();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -26,7 +21,6 @@
|
||||
|
||||
<div class="join w-full has-[:focus]:outline outline-2 outline-offset-2 outline-base-content/20">
|
||||
<input
|
||||
bind:this={input}
|
||||
type={show ? 'text' : 'password'}
|
||||
{value} {placeholder} {autofocus}
|
||||
on:input={e => value = e.target.value}
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
|
||||
// Extra 50ms so the window can finish disappearing before the redraw
|
||||
const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 100);
|
||||
const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 50);
|
||||
|
||||
let alert;
|
||||
let success = false;
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
let alert;
|
||||
let passphrase = '';
|
||||
|
||||
|
||||
let saving = false;
|
||||
async function unlock() {
|
||||
saving = true;
|
||||
@ -34,14 +34,9 @@
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let input;
|
||||
onMount(() => input.focus());
|
||||
</script>
|
||||
|
||||
|
||||
<svelte:window on:focus={input.focus} />
|
||||
|
||||
<div class="fixed top-0 w-full p-2 text-center">
|
||||
<h1 class="text-3xl font-bold">Creddy is locked</h1>
|
||||
</div>
|
||||
@ -57,11 +52,7 @@
|
||||
<ErrorAlert bind:this="{alert}" />
|
||||
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<PassphraseInput
|
||||
bind:this={input}
|
||||
bind:value={passphrase}
|
||||
placeholder="correct horse battery staple"
|
||||
/>
|
||||
<PassphraseInput autofocus="true" bind:value={passphrase} placeholder="correct horse battery staple" />
|
||||
</label>
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
|
@ -44,11 +44,7 @@
|
||||
<div class="space-y-1 mb-4">
|
||||
<h2 class="text-xl font-bold">
|
||||
{#if $appState.currentRequest.type === 'Aws'}
|
||||
{#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}
|
||||
{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}
|
||||
|
@ -15,6 +15,7 @@
|
||||
async function saveCredential() {
|
||||
await invoke('save_credential', {record: local});
|
||||
dispatch('save', local);
|
||||
showDetails = false;
|
||||
}
|
||||
|
||||
async function copyText(evt) {
|
||||
|
@ -13,23 +13,20 @@
|
||||
|
||||
let name;
|
||||
let file;
|
||||
let privateKey = '';
|
||||
let passphrase = '';
|
||||
let showDetails = true;
|
||||
let mode = 'file';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let defaultPath = null;
|
||||
homeDir().then(d => defaultPath = `${d}/.ssh`);
|
||||
|
||||
|
||||
let alert;
|
||||
let saving = false;
|
||||
async function saveCredential() {
|
||||
saving = true;
|
||||
try {
|
||||
let key = await getKey();
|
||||
let key = await invoke('sshkey_from_file', {path: file.path, passphrase});
|
||||
const payload = {
|
||||
id: record.id,
|
||||
name,
|
||||
@ -44,40 +41,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
|
||||
<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)}>
|
||||
<ErrorAlert bind:this={alert} />
|
||||
|
||||
@ -89,20 +55,15 @@
|
||||
bind:value={name}
|
||||
>
|
||||
|
||||
{#if mode === 'file'}
|
||||
<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">File</span>
|
||||
<FileInput params={{defaultPath}} bind:value={file} on:update={() => name = file.name} />
|
||||
|
||||
<span class="justify-self-end">Passphrase</span>
|
||||
<PassphraseInput class="bg-transparent" bind:value={passphrase} />
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
{#if file?.path || privateKey !== ''}
|
||||
{#if file?.path}
|
||||
<button
|
||||
transition:fade={{duration: 100}}
|
||||
type="submit"
|
||||
|
@ -64,7 +64,7 @@
|
||||
{#if record.isNew}
|
||||
<NewSshKey {record} on:save on:save={handleSave} />
|
||||
{:else}
|
||||
<EditSshKey bind:local={local} {isModified} on:save={handleSave} on:save />
|
||||
<EditSshKey bind:local={local} {isModified} on:save />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
Loading…
Reference in New Issue
Block a user