Compare commits
11 Commits
v0.5.1
...
e4a7c62828
Author | SHA1 | Date | |
---|---|---|---|
e4a7c62828 | |||
0fc97d28e0 | |||
b1a5f9f11a | |||
295698e62f | |||
3b61aa924a | |||
02ba19d709 | |||
55801384eb | |||
27c2f467c4 | |||
cab5ec40cc | |||
5cf848f7fe | |||
a32e36be7e |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "creddy",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.4",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
241
src-tauri/Cargo.lock
generated
241
src-tauri/Cargo.lock
generated
@ -110,6 +110,55 @@ 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"
|
||||
@ -327,17 +376,6 @@ 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"
|
||||
@ -1023,42 +1061,43 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.25"
|
||||
version = "4.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
|
||||
checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags 1.3.2",
|
||||
"clap_builder",
|
||||
"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",
|
||||
"indexmap 1.9.3",
|
||||
"once_cell",
|
||||
"strsim 0.10.0",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.2.25"
|
||||
version = "4.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008"
|
||||
checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro-error",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.4"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
@ -1071,7 +1110,7 @@ dependencies = [
|
||||
"cocoa-foundation",
|
||||
"core-foundation",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
@ -1090,6 +1129,12 @@ 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"
|
||||
@ -1146,7 +1191,7 @@ dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-graphics-types",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@ -1196,7 +1241,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "creddy"
|
||||
version = "0.5.0"
|
||||
version = "0.5.4"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"auto-launch",
|
||||
@ -1204,20 +1249,23 @@ dependencies = [
|
||||
"aws-sdk-sts",
|
||||
"aws-smithy-types",
|
||||
"aws-types",
|
||||
"base64 0.22.1",
|
||||
"chacha20poly1305",
|
||||
"clap",
|
||||
"creddy_cli",
|
||||
"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",
|
||||
@ -1237,6 +1285,18 @@ 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"
|
||||
@ -1395,7 +1455,7 @@ dependencies = [
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim 0.11.1",
|
||||
"strsim",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
@ -1841,6 +1901,15 @@ 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"
|
||||
@ -1848,7 +1917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||
dependencies = [
|
||||
"foreign-types-macros",
|
||||
"foreign-types-shared",
|
||||
"foreign-types-shared 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1862,6 +1931,12 @@ 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"
|
||||
@ -2416,15 +2491,6 @@ 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"
|
||||
@ -2747,6 +2813,12 @@ 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"
|
||||
@ -3426,12 +3498,50 @@ 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"
|
||||
@ -3459,12 +3569,6 @@ 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"
|
||||
@ -4562,9 +4666,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.118"
|
||||
version = "1.0.120"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4"
|
||||
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
|
||||
dependencies = [
|
||||
"itoa 1.0.11",
|
||||
"ryu",
|
||||
@ -4788,7 +4892,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"foreign-types",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
"objc2",
|
||||
@ -5181,12 +5285,6 @@ 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"
|
||||
@ -5674,21 +5772,6 @@ 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"
|
||||
@ -6160,6 +6243,12 @@ 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.1"
|
||||
version = "0.5.4"
|
||||
description = "A friendly AWS credentials manager"
|
||||
authors = ["Joseph Montanaro"]
|
||||
license = ""
|
||||
@ -9,37 +9,40 @@ 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]
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
creddy_cli = { path = "./creddy_cli" }
|
||||
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"] }
|
||||
@ -55,9 +58,16 @@ 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
|
||||
@ -67,8 +77,5 @@ 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
|
||||
|
12
src-tauri/creddy_cli/Cargo.toml
Normal file
12
src-tauri/creddy_cli/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[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 }
|
27
src-tauri/creddy_cli/src/cli/docker.rs
Normal file
27
src-tauri/creddy_cli/src/cli/docker.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::io;
|
||||
|
||||
use anyhow::bail;
|
||||
|
||||
use crate::proto::{CliResponse, DockerCredential};
|
||||
use super::{
|
||||
CliCredential,
|
||||
CliRequest,
|
||||
GlobalArgs
|
||||
};
|
||||
|
||||
|
||||
pub fn docker_store(global_args: GlobalArgs) -> anyhow::Result<()> {
|
||||
let input: DockerCredential = serde_json::from_reader(io::stdin())?;
|
||||
dbg!(&input);
|
||||
|
||||
let req = CliRequest::SaveCredential {
|
||||
name: input.username.clone(),
|
||||
is_default: false, // is_default doesn't really mean anything for Docker credentials
|
||||
credential: CliCredential::Docker(input),
|
||||
};
|
||||
|
||||
match super::make_request(global_args.server_addr, &req)?? {
|
||||
CliResponse::Empty => Ok(()),
|
||||
r => bail!("Unexpected response from server: {r}"),
|
||||
}
|
||||
}
|
234
src-tauri/creddy_cli/src/cli/mod.rs
Normal file
234
src-tauri/creddy_cli/src/cli/mod.rs
Normal file
@ -0,0 +1,234 @@
|
||||
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,
|
||||
};
|
||||
|
||||
mod docker;
|
||||
|
||||
|
||||
#[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),
|
||||
/// Interact with Docker credentials via the docker-credential-helper protocol
|
||||
#[command(subcommand)]
|
||||
Docker(DockerCmd),
|
||||
}
|
||||
|
||||
|
||||
#[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,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum DockerCmd {
|
||||
/// Get a stored Docker credential
|
||||
Get,
|
||||
/// Store a new Docker credential
|
||||
Store,
|
||||
/// Remove a stored Docker credential
|
||||
Erase,
|
||||
}
|
||||
|
||||
|
||||
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}"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn docker_credential_helper(cmd: DockerCmd, global_args: GlobalArgs) -> anyhow::Result<()> {
|
||||
match cmd {
|
||||
DockerCmd::Get => todo!(),
|
||||
DockerCmd::Store => docker::docker_store(global_args),
|
||||
DockerCmd::Erase => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
// (probably this should be modeled differently)
|
||||
#[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)
|
||||
}
|
41
src-tauri/creddy_cli/src/lib.rs
Normal file
41
src-tauri/creddy_cli/src/lib.rs
Normal file
@ -0,0 +1,41 @@
|
||||
mod cli;
|
||||
pub use cli::{
|
||||
Cli,
|
||||
Action,
|
||||
exec,
|
||||
get,
|
||||
invoke_shortcut,
|
||||
docker_credential_helper,
|
||||
};
|
||||
|
||||
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}")
|
||||
}
|
||||
}
|
36
src-tauri/creddy_cli/src/main.rs
Normal file
36
src-tauri/creddy_cli/src/main.rs
Normal file
@ -0,0 +1,36 @@
|
||||
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),
|
||||
Some(Action::Docker(cmd)) => creddy_cli::docker_credential_helper(cmd, 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(())
|
||||
}
|
108
src-tauri/creddy_cli/src/proto.rs
Normal file
108
src-tauri/creddy_cli/src/proto.rs
Normal file
@ -0,0 +1,108 @@
|
||||
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,
|
||||
},
|
||||
SaveCredential {
|
||||
name: String,
|
||||
is_default: bool,
|
||||
credential: CliCredential,
|
||||
},
|
||||
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::Credential(CliCredential::Docker(_)) => write!(f, "Credential (Docker)"),
|
||||
CliResponse::Empty => write!(f, "Empty"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliCredential {
|
||||
AwsBase(AwsBaseCredential),
|
||||
AwsSession(AwsSessionCredential),
|
||||
Docker(DockerCredential),
|
||||
}
|
||||
|
||||
|
||||
#[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, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct DockerCredential {
|
||||
#[serde(rename = "ServerURL")]
|
||||
pub server_url: String,
|
||||
pub username: String,
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
|
||||
#[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 {}
|
11
src-tauri/migrations/20240919135710_docker_creds.sql
Normal file
11
src-tauri/migrations/20240919135710_docker_creds.sql
Normal file
@ -0,0 +1,11 @@
|
||||
CREATE TABLE docker_credentials (
|
||||
id BLOB UNIQUE NOT NULL,
|
||||
-- The Docker credential helper protocol only sends the server_url, so
|
||||
-- we should guarantee that we will only ever have one matching credential.
|
||||
-- Also, it's easier to go from unique -> not-unique than vice versa if we
|
||||
-- decide that's necessary in the future
|
||||
server_url TEXT UNIQUE NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
secret_enc BLOB NOT NULL,
|
||||
nonce BLOB NOT NULL
|
||||
);
|
@ -1,47 +0,0 @@
|
||||
// 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(())
|
||||
}
|
@ -1,213 +0,0 @@
|
||||
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::srv::{
|
||||
self,
|
||||
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")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("name")
|
||||
.help("If unspecified, use default 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("name")
|
||||
.short('n')
|
||||
.long("name")
|
||||
.help("If unspecified, use default 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 name = args.get_one("name").cloned();
|
||||
let base = *args.get_one("base").unwrap_or(&false);
|
||||
|
||||
let output = match make_request(&Request::GetAwsCredentials { name, base })? {
|
||||
Response::AwsBase(creds) => serde_json::to_string(&creds).unwrap(),
|
||||
Response::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 name = args.get_one("name").cloned();
|
||||
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 { name, 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 {
|
||||
let addr = srv::addr("creddy-server");
|
||||
match ClientOptions::new().open(&addr) {
|
||||
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> {
|
||||
let path = srv::addr("creddy-server");
|
||||
UnixStream::connect(&path).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,10 +185,16 @@ 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(),
|
||||
@ -203,19 +209,6 @@ mod tests {
|
||||
)
|
||||
}
|
||||
|
||||
fn test_uuid() -> Uuid {
|
||||
Uuid::try_parse("00000000-0000-0000-0000-000000000000").unwrap()
|
||||
}
|
||||
|
||||
fn test_uuid_2() -> Uuid {
|
||||
Uuid::try_parse("ffffffff-ffff-ffff-ffff-ffffffffffff").unwrap()
|
||||
}
|
||||
|
||||
fn test_uuid_random() -> Uuid {
|
||||
let bytes = Crypto::salt();
|
||||
Uuid::from_slice(&bytes[..16]).unwrap()
|
||||
}
|
||||
|
||||
|
||||
#[sqlx::test(fixtures("aws_credentials"))]
|
||||
async fn test_load(pool: SqlitePool) {
|
||||
@ -254,5 +247,99 @@ 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);
|
||||
}
|
||||
}
|
||||
|
197
src-tauri/src/credentials/docker.rs
Normal file
197
src-tauri/src/credentials/docker.rs
Normal file
@ -0,0 +1,197 @@
|
||||
use chacha20poly1305::XNonce;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use sqlx::{
|
||||
FromRow,
|
||||
Sqlite,
|
||||
Transaction,
|
||||
types::Uuid,
|
||||
};
|
||||
|
||||
use super::{Credential, Crypto, PersistentCredential};
|
||||
|
||||
use crate::errors::*;
|
||||
|
||||
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct DockerRow {
|
||||
id: Uuid,
|
||||
server_url: String,
|
||||
username: String,
|
||||
secret_enc: Vec<u8>,
|
||||
nonce: Vec<u8>,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct DockerCredential {
|
||||
#[serde(rename = "ServerURL")]
|
||||
pub server_url: String,
|
||||
pub username: String,
|
||||
pub secret: String,
|
||||
}
|
||||
|
||||
|
||||
impl PersistentCredential for DockerCredential {
|
||||
type Row = DockerRow;
|
||||
|
||||
fn type_name() -> &'static str { "docker" }
|
||||
|
||||
fn into_credential(self) -> Credential { Credential::Docker(self) }
|
||||
|
||||
fn row_id(row: &DockerRow) -> Uuid { row.id }
|
||||
|
||||
fn from_row(row: DockerRow, crypto: &Crypto) -> Result<Self, LoadCredentialsError> {
|
||||
let nonce = XNonce::clone_from_slice(&row.nonce);
|
||||
let secret_bytes = crypto.decrypt(&nonce, &row.secret_enc)?;
|
||||
let secret = String::from_utf8(secret_bytes)
|
||||
.map_err(|_| LoadCredentialsError::InvalidData)?;
|
||||
|
||||
Ok(DockerCredential {
|
||||
server_url: row.server_url,
|
||||
username: row.username,
|
||||
secret
|
||||
})
|
||||
}
|
||||
|
||||
async fn save_details(&self, id: &Uuid, crypto: &Crypto, txn: &mut Transaction<'_, Sqlite>) -> Result<(), SaveCredentialsError> {
|
||||
let (nonce, ciphertext) = crypto.encrypt(self.secret.as_bytes())?;
|
||||
let nonce_bytes = &nonce.as_slice();
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT OR REPLACE INTO docker_credentials (
|
||||
id,
|
||||
server_url,
|
||||
username,
|
||||
secret_enc,
|
||||
nonce
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)",
|
||||
id, self.server_url, self.username, ciphertext, nonce_bytes,
|
||||
).execute(&mut **txn).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::credentials::CredentialRecord;
|
||||
use creddy_cli::proto::DockerCredential as CliDockerCredential;
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::types::uuid::uuid;
|
||||
|
||||
|
||||
|
||||
fn test_credential() -> DockerCredential {
|
||||
DockerCredential {
|
||||
server_url: "https://registry.jfmonty2.com".into(),
|
||||
username: "joe@jfmonty2.com".into(),
|
||||
secret: "correct horse battery staple".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_credential_2() -> DockerCredential {
|
||||
DockerCredential {
|
||||
server_url: "https://index.docker.io/v1".into(),
|
||||
username: "test@example.com".into(),
|
||||
secret: "a very secure passphrase".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_record() -> CredentialRecord {
|
||||
CredentialRecord {
|
||||
id: uuid!("00000000-0000-0000-0000-000000000000"),
|
||||
name: "docker_test".into(),
|
||||
is_default: false,
|
||||
credential: Credential::Docker(test_credential()),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_record_2() -> CredentialRecord {
|
||||
CredentialRecord {
|
||||
id: uuid!("ffffffff-ffff-ffff-ffff-ffffffffffff"),
|
||||
name: "docker_test_2".into(),
|
||||
is_default: false,
|
||||
credential: Credential::Docker(test_credential_2()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_save(pool: SqlitePool) {
|
||||
let crypt = Crypto::random();
|
||||
test_record().save(&crypt, &pool).await
|
||||
.expect("Failed to save record");
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("docker_credentials"))]
|
||||
fn test_load(pool: SqlitePool) {
|
||||
let crypt = Crypto::fixed();
|
||||
let id = uuid!("00000000-0000-0000-0000-000000000000");
|
||||
let loaded = DockerCredential::load(&id, &crypt, &pool).await
|
||||
.expect("Failed to load record");
|
||||
|
||||
assert_eq!(test_credential(), loaded);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("docker_credentials"))]
|
||||
async fn test_overwrite(pool: SqlitePool) {
|
||||
let crypt = Crypto::fixed();
|
||||
let mut record = test_record_2();
|
||||
// give it the same id as test_record so that it overwrites
|
||||
let id = uuid!("00000000-0000-0000-0000-000000000000");
|
||||
record.id = id;
|
||||
record.save(&crypt, &pool).await
|
||||
.expect("Failed to overwrite original record with second record");
|
||||
|
||||
let loaded = DockerCredential::load(&id, &crypt, &pool).await
|
||||
.expect("Failed to load again after overwriting");
|
||||
|
||||
assert_eq!(test_credential_2(), loaded);
|
||||
}
|
||||
|
||||
#[sqlx::test(fixtures("docker_credentials"))]
|
||||
async fn test_list(pool: SqlitePool) {
|
||||
let crypt = Crypto::fixed();
|
||||
let records = CredentialRecord::list(&crypt, &pool).await
|
||||
.expect("Failed to list credentials");
|
||||
|
||||
assert_eq!(test_record(), records[0]);
|
||||
}
|
||||
|
||||
|
||||
// make sure that CLI credentials and app credentials don't drift apart
|
||||
#[test]
|
||||
fn test_cli_to_app() {
|
||||
let cli_creds = CliDockerCredential {
|
||||
server_url: "https://registry.jfmonty2.com".into(),
|
||||
username: "joe@jfmonty2.com".into(),
|
||||
secret: "correct horse battery staple".into(),
|
||||
};
|
||||
|
||||
let json = serde_json::to_string(&cli_creds).unwrap();
|
||||
let computed: DockerCredential = serde_json::from_str(&json)
|
||||
.expect("Failed to deserialize Docker credentials from CLI -> main app");
|
||||
|
||||
assert_eq!(test_credential(), computed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_app_to_cli() {
|
||||
let app_creds = test_credential();
|
||||
let json = serde_json::to_string(&app_creds).unwrap();
|
||||
|
||||
let computed: CliDockerCredential = serde_json::from_str(&json)
|
||||
.expect("Failed to deserialize Docker credentials from main app -> CLI");
|
||||
|
||||
let expected = CliDockerCredential {
|
||||
server_url: "https://registry.jfmonty2.com".into(),
|
||||
username: "joe@jfmonty2.com".into(),
|
||||
secret: "correct horse battery staple".into(),
|
||||
};
|
||||
assert_eq!(expected, computed);
|
||||
}
|
||||
}
|
11
src-tauri/src/credentials/fixtures/docker_credentials.sql
Normal file
11
src-tauri/src/credentials/fixtures/docker_credentials.sql
Normal file
@ -0,0 +1,11 @@
|
||||
INSERT INTO credentials (id, name, credential_type, is_default, created_at)
|
||||
VALUES (X'00000000000000000000000000000000', 'docker_test', 'docker', 0, 1726756380);
|
||||
|
||||
INSERT INTO docker_credentials (id, server_url, username, secret_enc, nonce)
|
||||
VALUES (
|
||||
X'00000000000000000000000000000000',
|
||||
'https://registry.jfmonty2.com',
|
||||
'joe@jfmonty2.com',
|
||||
X'C0B36EE54539D4113A8F73E99FB96B2BF4D87E91F7C3B48256C07E83E3E7EC738888B2FDE2B4DB0BE48BEFDE',
|
||||
X'C5F7F627BBE09A1BB275BE8D2390596C76143881A7766E60'
|
||||
);
|
@ -1,3 +1,11 @@
|
||||
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
|
||||
(
|
||||
|
@ -17,6 +17,9 @@ pub use aws::{AwsBaseCredential, AwsSessionCredential};
|
||||
mod crypto;
|
||||
pub use crypto::Crypto;
|
||||
|
||||
mod docker;
|
||||
pub use docker::DockerCredential;
|
||||
|
||||
mod record;
|
||||
pub use record::CredentialRecord;
|
||||
|
||||
@ -32,6 +35,7 @@ pub use ssh::SshKey;
|
||||
pub enum Credential {
|
||||
AwsBase(AwsBaseCredential),
|
||||
AwsSession(AwsSessionCredential),
|
||||
Docker(DockerCredential),
|
||||
Ssh(SshKey),
|
||||
}
|
||||
|
||||
@ -99,15 +103,15 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized {
|
||||
async fn list(crypto: &Crypto, pool: &SqlitePool) -> Result<Vec<(Uuid, Credential)>, LoadCredentialsError> {
|
||||
let q = format!(
|
||||
"SELECT details.*
|
||||
FROM
|
||||
FROM
|
||||
{} details
|
||||
JOIN credentials c
|
||||
ON c.id = details.id
|
||||
ORDER BY c.created_at",
|
||||
ORDER BY c.created_at",
|
||||
Self::table_name(),
|
||||
);
|
||||
let mut rows = sqlx::query_as::<_, Self::Row>(&q).fetch(pool);
|
||||
|
||||
|
||||
let mut creds = Vec::new();
|
||||
while let Some(row) = rows.try_next().await? {
|
||||
let id = Self::row_id(&row);
|
||||
|
@ -20,6 +20,7 @@ use super::{
|
||||
AwsBaseCredential,
|
||||
Credential,
|
||||
Crypto,
|
||||
DockerCredential,
|
||||
PersistentCredential,
|
||||
SshKey,
|
||||
};
|
||||
@ -51,6 +52,7 @@ impl CredentialRecord {
|
||||
let type_name = match &self.credential {
|
||||
Credential::AwsBase(_) => AwsBaseCredential::type_name(),
|
||||
Credential::Ssh(_) => SshKey::type_name(),
|
||||
Credential::Docker(_) => DockerCredential::type_name(),
|
||||
_ => return Err(SaveCredentialsError::NotPersistent),
|
||||
};
|
||||
|
||||
@ -86,6 +88,7 @@ impl CredentialRecord {
|
||||
match &self.credential {
|
||||
Credential::AwsBase(b) => b.save_details(&self.id, crypto, &mut txn).await,
|
||||
Credential::Ssh(s) => s.save_details(&self.id, crypto, &mut txn).await,
|
||||
Credential::Docker(d) => d.save_details(&self.id, crypto, &mut txn).await,
|
||||
_ => Err(SaveCredentialsError::NotPersistent),
|
||||
}?;
|
||||
|
||||
@ -112,15 +115,16 @@ impl CredentialRecord {
|
||||
Ok(Self::from_parts(row, credential))
|
||||
}
|
||||
|
||||
// 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)?;
|
||||
#[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)?;
|
||||
|
||||
// Self::load_credential(row, crypto, pool).await
|
||||
// }
|
||||
Self::load_credential(row, crypto, pool).await
|
||||
}
|
||||
|
||||
pub async fn load_by_name(name: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> {
|
||||
let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE name = ?")
|
||||
@ -134,7 +138,7 @@ impl CredentialRecord {
|
||||
|
||||
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)
|
||||
@ -166,6 +170,11 @@ impl CredentialRecord {
|
||||
.ok_or(LoadCredentialsError::InvalidData)?;
|
||||
records.push(Self::from_parts(parent, credential));
|
||||
}
|
||||
for (id, credential) in DockerCredential::list(crypto, pool).await? {
|
||||
let parent = parent_map.remove(&id)
|
||||
.ok_or(LoadCredentialsError::InvalidData)?;
|
||||
records.push(Self::from_parts(parent, credential));
|
||||
}
|
||||
|
||||
Ok(records)
|
||||
}
|
||||
@ -419,7 +428,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,6 +12,8 @@ use serde::ser::{
|
||||
SerializeStruct,
|
||||
};
|
||||
use serde::de::{self, Visitor};
|
||||
use sha2::{Sha256, Sha512};
|
||||
use signature::{Signer, SignatureEncoding};
|
||||
use sqlx::{
|
||||
FromRow,
|
||||
Sqlite,
|
||||
@ -19,11 +21,15 @@ use sqlx::{
|
||||
Transaction,
|
||||
types::Uuid,
|
||||
};
|
||||
use ssh_agent_lib::proto::message::Identity;
|
||||
use ssh_agent_lib::proto::message::{
|
||||
Identity,
|
||||
SignRequest,
|
||||
};
|
||||
use ssh_encoding::Encode;
|
||||
use ssh_key::{
|
||||
Algorithm,
|
||||
LineEnding,
|
||||
private::PrivateKey,
|
||||
private::{PrivateKey, KeypairData},
|
||||
public::PublicKey,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
@ -93,7 +99,7 @@ impl SshKey {
|
||||
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
|
||||
@ -119,6 +125,33 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -135,7 +168,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)
|
||||
@ -265,8 +298,9 @@ 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 {
|
||||
@ -308,7 +342,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()),
|
||||
@ -326,7 +360,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()),
|
||||
@ -344,7 +378,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()),
|
||||
@ -362,7 +396,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()),
|
||||
@ -402,11 +436,14 @@ mod tests {
|
||||
#[sqlx::test]
|
||||
async fn test_save_db(pool: SqlitePool) {
|
||||
let crypto = Crypto::random();
|
||||
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");
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@ -414,7 +451,7 @@ mod tests {
|
||||
async fn test_load_db(pool: SqlitePool) {
|
||||
let crypto = Crypto::fixed();
|
||||
let id = uuid!("11111111-1111-1111-1111-111111111111");
|
||||
let k = SshKey::load(&id, &crypto, &pool).await
|
||||
SshKey::load(&id, &crypto, &pool).await
|
||||
.expect("Failed to load SSH key from database");
|
||||
}
|
||||
|
||||
@ -422,13 +459,18 @@ mod tests {
|
||||
#[sqlx::test]
|
||||
async fn test_save_load_db(pool: SqlitePool) {
|
||||
let crypto = Crypto::random();
|
||||
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 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 known = ed25519_plain();
|
||||
|
||||
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,6 +195,10 @@ 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),
|
||||
}
|
||||
|
||||
|
||||
@ -366,7 +370,7 @@ pub enum RequestError {
|
||||
#[error("Error response from server: {0}")]
|
||||
Server(ServerError),
|
||||
#[error("Unexpected response from server")]
|
||||
Unexpected(crate::srv::Response),
|
||||
Unexpected(crate::srv::CliResponse),
|
||||
#[error("The server did not respond with valid JSON")]
|
||||
InvalidJson(#[from] serde_json::Error),
|
||||
#[error("Error reading/writing stream: {0}")]
|
||||
|
@ -44,21 +44,23 @@ pub async fn load_bytes(pool: &SqlitePool, name: &str) -> Result<Option<Vec<u8>>
|
||||
}
|
||||
|
||||
|
||||
// pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> {
|
||||
// sqlx::query!("DELETE FROM kv WHERE name = ?", name)
|
||||
// .execute(pool)
|
||||
// .await?;
|
||||
// Ok(())
|
||||
// }
|
||||
// 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_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);
|
||||
@ -83,7 +85,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,
|
||||
@ -187,7 +189,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,5 +1,4 @@
|
||||
pub mod app;
|
||||
pub mod cli;
|
||||
mod config;
|
||||
mod credentials;
|
||||
pub mod errors;
|
||||
|
@ -3,23 +3,25 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
|
||||
use creddy::{
|
||||
app,
|
||||
cli,
|
||||
errors::ShowError,
|
||||
};
|
||||
use creddy_cli::{Action, Cli};
|
||||
|
||||
|
||||
fn main() {
|
||||
let res = match cli::parser().get_matches().subcommand() {
|
||||
None | Some(("run", _)) => {
|
||||
let cli = Cli::parse();
|
||||
let res = match cli.action {
|
||||
None | Some(Action::Run) => {
|
||||
app::run().error_popup("Creddy encountered an error");
|
||||
Ok(())
|
||||
},
|
||||
Some(("get", m)) => cli::get(m),
|
||||
Some(("exec", m)) => cli::exec(m),
|
||||
Some(("shortcut", m)) => cli::invoke_shortcut(m),
|
||||
_ => unreachable!(),
|
||||
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(Action::Docker(cmd)) => creddy_cli::docker_credential_helper(cmd, cli.global_args),
|
||||
};
|
||||
|
||||
if let Err(e) = res {
|
||||
|
@ -1,5 +1,4 @@
|
||||
use futures::SinkExt;
|
||||
use signature::Signer;
|
||||
use ssh_agent_lib::agent::MessageCodec;
|
||||
use ssh_agent_lib::proto::message::{
|
||||
Message,
|
||||
@ -36,12 +35,21 @@ async fn handle(
|
||||
adapter.send(resp).await?;
|
||||
},
|
||||
Message::SignRequest(req) => {
|
||||
// CloseWaiter could corrupt the framing, but this doesn't matter
|
||||
// since we don't plan to pull any more frames out of the stream
|
||||
// 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?;
|
||||
break;
|
||||
|
||||
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?,
|
||||
};
|
||||
@ -93,15 +101,8 @@ async fn sign_request(
|
||||
}
|
||||
|
||||
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 sig = key.sign_request(&req)?;
|
||||
Ok(Message::SignResponse(sig))
|
||||
};
|
||||
|
||||
let res = proceed.await;
|
||||
@ -112,10 +113,3 @@ async fn sign_request(
|
||||
lease.release();
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
fn encode_string(buf: &mut Vec<u8>, s: &[u8]) {
|
||||
let len = s.len() as u32;
|
||||
buf.extend(len.to_be_bytes());
|
||||
buf.extend(s);
|
||||
}
|
||||
|
@ -9,8 +9,9 @@ use crate::shortcuts::{self, ShortcutAction};
|
||||
use crate::state::AppState;
|
||||
use super::{
|
||||
CloseWaiter,
|
||||
Request,
|
||||
Response,
|
||||
CliCredential,
|
||||
CliRequest,
|
||||
CliResponse,
|
||||
Stream,
|
||||
};
|
||||
|
||||
@ -43,13 +44,12 @@ async fn handle(
|
||||
let waiter = CloseWaiter { stream: &mut stream };
|
||||
|
||||
|
||||
let req: Request = serde_json::from_slice(&buf)?;
|
||||
let req: CliRequest = serde_json::from_slice(&buf)?;
|
||||
let res = match req {
|
||||
Request::GetAwsCredentials { name, base } => get_aws_credentials(
|
||||
CliRequest::GetCredential{ name, base } => get_aws_credentials(
|
||||
name, base, client, app_handle, waiter
|
||||
).await,
|
||||
Request::InvokeShortcut(action) => invoke_shortcut(action).await,
|
||||
Request::GetSshSignature(_) => return Err(HandlerError::Denied),
|
||||
CliRequest::InvokeShortcut(action) => invoke_shortcut(action).await,
|
||||
};
|
||||
|
||||
// doesn't make sense to send the error to the client if the client has already left
|
||||
@ -63,9 +63,9 @@ async fn handle(
|
||||
}
|
||||
|
||||
|
||||
async fn invoke_shortcut(action: ShortcutAction) -> Result<Response, HandlerError> {
|
||||
async fn invoke_shortcut(action: ShortcutAction) -> Result<CliResponse, HandlerError> {
|
||||
shortcuts::exec_shortcut(action);
|
||||
Ok(Response::Empty)
|
||||
Ok(CliResponse::Empty)
|
||||
}
|
||||
|
||||
|
||||
@ -75,7 +75,7 @@ async fn get_aws_credentials(
|
||||
client: Client,
|
||||
app_handle: AppHandle,
|
||||
mut waiter: CloseWaiter<'_>,
|
||||
) -> Result<Response, HandlerError> {
|
||||
) -> Result<CliResponse, HandlerError> {
|
||||
let state = app_handle.state::<AppState>();
|
||||
let rehide_ms = {
|
||||
let config = state.config.read().await;
|
||||
@ -108,11 +108,11 @@ async fn get_aws_credentials(
|
||||
Approval::Approved => {
|
||||
if response.base {
|
||||
let creds = state.get_aws_base(name).await?;
|
||||
Ok(Response::AwsBase(creds))
|
||||
Ok(CliResponse::Credential(CliCredential::AwsBase(creds)))
|
||||
}
|
||||
else {
|
||||
let creds = state.get_aws_session(name).await?;
|
||||
Ok(Response::AwsSession(creds.clone()))
|
||||
let creds = state.get_aws_session(name).await?.clone();
|
||||
Ok(CliResponse::Credential(CliCredential::AwsSession(creds)))
|
||||
}
|
||||
},
|
||||
Approval::Denied => Err(HandlerError::Denied),
|
||||
@ -129,4 +129,4 @@ async fn get_aws_credentials(
|
||||
|
||||
lease.release();
|
||||
result
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ use tauri::{
|
||||
};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use ssh_agent_lib::proto::message::SignRequest;
|
||||
|
||||
use crate::credentials::{AwsBaseCredential, AwsSessionCredential};
|
||||
use crate::errors::*;
|
||||
@ -15,25 +14,32 @@ use crate::shortcuts::ShortcutAction;
|
||||
pub mod creddy_server;
|
||||
pub mod agent;
|
||||
use platform::Stream;
|
||||
pub use platform::addr;
|
||||
|
||||
|
||||
// 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 Request {
|
||||
GetAwsCredentials {
|
||||
pub enum CliRequest {
|
||||
GetCredential {
|
||||
name: Option<String>,
|
||||
base: bool,
|
||||
},
|
||||
GetSshSignature(SignRequest),
|
||||
InvokeShortcut(ShortcutAction),
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum Response {
|
||||
pub enum CliResponse {
|
||||
Credential(CliCredential),
|
||||
Empty,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub enum CliCredential {
|
||||
AwsBase(AwsBaseCredential),
|
||||
AwsSession(AwsSessionCredential),
|
||||
Empty,
|
||||
}
|
||||
|
||||
|
||||
@ -92,7 +98,7 @@ mod platform {
|
||||
pub type Stream = UnixStream;
|
||||
|
||||
pub fn bind(sock_name: &str) -> std::io::Result<(UnixListener, PathBuf)> {
|
||||
let path = addr(sock_name);
|
||||
let path = creddy_cli::server_addr(sock_name);
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(_) => (),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => (),
|
||||
@ -112,14 +118,6 @@ mod platform {
|
||||
|
||||
Ok((stream, pid))
|
||||
}
|
||||
|
||||
|
||||
pub fn addr(sock_name: &str) -> PathBuf {
|
||||
let mut path = dirs::runtime_dir()
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp"));
|
||||
path.push(format!("{sock_name}.sock"));
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -140,7 +138,7 @@ mod platform {
|
||||
pub type Stream = NamedPipeServer;
|
||||
|
||||
pub fn bind(sock_name: &str) -> std::io::Result<(String, NamedPipeServer)> {
|
||||
let addr = addr(sock_name);
|
||||
let addr = creddy_cli::server_addr(sock_name);
|
||||
let listener = ServerOptions::new()
|
||||
.first_pipe_instance(true)
|
||||
.create(&addr)?;
|
||||
@ -163,8 +161,4 @@ mod platform {
|
||||
unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? };
|
||||
Ok((stream, pid))
|
||||
}
|
||||
|
||||
pub fn addr(sock_name: &str) -> String {
|
||||
format!(r"\\.\pipe\{sock_name}")
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +50,7 @@
|
||||
}
|
||||
},
|
||||
"productName": "creddy",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.4",
|
||||
"identifier": "creddy",
|
||||
"plugins": {},
|
||||
"app": {
|
||||
@ -85,4 +85,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
let alert;
|
||||
let passphrase = '';
|
||||
|
||||
|
||||
let saving = false;
|
||||
async function unlock() {
|
||||
saving = true;
|
||||
@ -40,6 +40,8 @@
|
||||
</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>
|
||||
|
@ -60,7 +60,7 @@
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="join-item flex-1 btn border border-primary"
|
||||
class="join-item flex-1 btn border border-primary hover:border-primary"
|
||||
class:btn-primary={mode === 'file'}
|
||||
on:click={() => mode = 'file'}
|
||||
>
|
||||
@ -69,7 +69,7 @@
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class="join-item flex-1 btn border border-primary"
|
||||
class="join-item flex-1 btn border border-primary hover:border-primary"
|
||||
class:btn-primary={mode === 'direct'}
|
||||
on:click={() => mode = 'direct'}
|
||||
>
|
||||
|
Reference in New Issue
Block a user