Compare commits
	
		
			24 Commits
		
	
	
		
			ssh-agent
			...
			0cfa9fc07a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 0cfa9fc07a | |||
| 9e9bc2b0ae | |||
| 07bf98e522 | |||
| e0e758554c | |||
| 479a0a96eb | |||
| c6e22fc91b | |||
| 9bc9cb56c1 | |||
| 8bcdc5420a | |||
| 0a355c299b | |||
| 192d9058c3 | |||
| b88b32d0f1 | |||
| 12c97c4a7d | |||
| 97528d65d6 | |||
| 295698e62f | |||
| 3b61aa924a | |||
| 02ba19d709 | |||
| 55801384eb | |||
| 27c2f467c4 | |||
| cab5ec40cc | |||
| 5cf848f7fe | |||
| a32e36be7e | |||
| 10231df860 | |||
| ae93a57aab | |||
| 9fd355b68e | 
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "creddy", |   "name": "creddy", | ||||||
|   "version": "0.4.9", |   "version": "0.6.1", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "vite", |     "dev": "vite", | ||||||
|     "build": "vite build", |     "build": "vite build", | ||||||
|   | |||||||
							
								
								
									
										287
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										287
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -110,6 +110,55 @@ dependencies = [ | |||||||
|  "libc", |  "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]] | [[package]] | ||||||
| name = "anyhow" | name = "anyhow" | ||||||
| version = "1.0.86" | version = "1.0.86" | ||||||
| @@ -169,30 +218,6 @@ dependencies = [ | |||||||
|  "pin-project-lite", |  "pin-project-lite", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "async-executor" |  | ||||||
| version = "1.12.0" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "c8828ec6e544c02b0d6691d21ed9f9218d0384a82542855073c2a3f58304aaf0" |  | ||||||
| dependencies = [ |  | ||||||
|  "async-task", |  | ||||||
|  "concurrent-queue", |  | ||||||
|  "fastrand", |  | ||||||
|  "futures-lite", |  | ||||||
|  "slab", |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "async-fs" |  | ||||||
| version = "2.1.2" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" |  | ||||||
| dependencies = [ |  | ||||||
|  "async-lock", |  | ||||||
|  "blocking", |  | ||||||
|  "futures-lite", |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "async-io" | name = "async-io" | ||||||
| version = "2.3.3" | version = "2.3.3" | ||||||
| @@ -327,17 +352,6 @@ version = "1.1.2" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" | 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]] | [[package]] | ||||||
| name = "auto-launch" | name = "auto-launch" | ||||||
| version = "0.4.0" | version = "0.4.0" | ||||||
| @@ -1023,42 +1037,43 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "clap" | name = "clap" | ||||||
| version = "3.2.25" | version = "4.5.9" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" | checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "atty", |  "clap_builder", | ||||||
|  "bitflags 1.3.2", |  | ||||||
|  "clap_derive", |  "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", |  "clap_lex", | ||||||
|  "indexmap 1.9.3", |  "strsim", | ||||||
|  "once_cell", |  | ||||||
|  "strsim 0.10.0", |  | ||||||
|  "termcolor", |  | ||||||
|  "textwrap", |  | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "clap_derive" | name = "clap_derive" | ||||||
| version = "3.2.25" | version = "4.5.8" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" | checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "heck 0.4.1", |  "heck 0.5.0", | ||||||
|  "proc-macro-error", |  | ||||||
|  "proc-macro2", |  "proc-macro2", | ||||||
|  "quote", |  "quote", | ||||||
|  "syn 1.0.109", |  "syn 2.0.68", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "clap_lex" | name = "clap_lex" | ||||||
| version = "0.2.4" | version = "0.7.1" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" | checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" | ||||||
| dependencies = [ |  | ||||||
|  "os_str_bytes", |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "cocoa" | name = "cocoa" | ||||||
| @@ -1071,7 +1086,7 @@ dependencies = [ | |||||||
|  "cocoa-foundation", |  "cocoa-foundation", | ||||||
|  "core-foundation", |  "core-foundation", | ||||||
|  "core-graphics", |  "core-graphics", | ||||||
|  "foreign-types", |  "foreign-types 0.5.0", | ||||||
|  "libc", |  "libc", | ||||||
|  "objc", |  "objc", | ||||||
| ] | ] | ||||||
| @@ -1090,6 +1105,12 @@ dependencies = [ | |||||||
|  "objc", |  "objc", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "colorchoice" | ||||||
|  | version = "1.0.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "combine" | name = "combine" | ||||||
| version = "4.6.7" | version = "4.6.7" | ||||||
| @@ -1146,7 +1167,7 @@ dependencies = [ | |||||||
|  "bitflags 1.3.2", |  "bitflags 1.3.2", | ||||||
|  "core-foundation", |  "core-foundation", | ||||||
|  "core-graphics-types", |  "core-graphics-types", | ||||||
|  "foreign-types", |  "foreign-types 0.5.0", | ||||||
|  "libc", |  "libc", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| @@ -1196,7 +1217,7 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "creddy" | name = "creddy" | ||||||
| version = "0.4.9" | version = "0.6.1" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "argon2", |  "argon2", | ||||||
|  "auto-launch", |  "auto-launch", | ||||||
| @@ -1204,20 +1225,23 @@ dependencies = [ | |||||||
|  "aws-sdk-sts", |  "aws-sdk-sts", | ||||||
|  "aws-smithy-types", |  "aws-smithy-types", | ||||||
|  "aws-types", |  "aws-types", | ||||||
|  "base64 0.22.1", |  | ||||||
|  "chacha20poly1305", |  "chacha20poly1305", | ||||||
|  "clap", |  "creddy_cli", | ||||||
|  "dirs 5.0.1", |  "dirs 5.0.1", | ||||||
|  "futures", |  "futures", | ||||||
|  "is-terminal", |  "is-terminal", | ||||||
|  "once_cell", |  "once_cell", | ||||||
|  |  "openssl", | ||||||
|  "rfd 0.13.0", |  "rfd 0.13.0", | ||||||
|  |  "rsa", | ||||||
|  "serde", |  "serde", | ||||||
|  "serde_json", |  "serde_json", | ||||||
|  |  "sha2", | ||||||
|  "signature 2.2.0", |  "signature 2.2.0", | ||||||
|  "sodiumoxide", |  "sodiumoxide", | ||||||
|  "sqlx", |  "sqlx", | ||||||
|  "ssh-agent-lib", |  "ssh-agent-lib", | ||||||
|  |  "ssh-encoding", | ||||||
|  "ssh-key", |  "ssh-key", | ||||||
|  "strum", |  "strum", | ||||||
|  "strum_macros", |  "strum_macros", | ||||||
| @@ -1227,7 +1251,6 @@ dependencies = [ | |||||||
|  "tauri-plugin-dialog", |  "tauri-plugin-dialog", | ||||||
|  "tauri-plugin-global-shortcut", |  "tauri-plugin-global-shortcut", | ||||||
|  "tauri-plugin-os", |  "tauri-plugin-os", | ||||||
|  "tauri-plugin-single-instance", |  | ||||||
|  "thiserror", |  "thiserror", | ||||||
|  "time", |  "time", | ||||||
|  "tokio", |  "tokio", | ||||||
| @@ -1237,6 +1260,18 @@ dependencies = [ | |||||||
|  "windows 0.51.1", |  "windows 0.51.1", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "creddy_cli" | ||||||
|  | version = "0.6.0" | ||||||
|  | dependencies = [ | ||||||
|  |  "anyhow", | ||||||
|  |  "clap", | ||||||
|  |  "dirs 5.0.1", | ||||||
|  |  "serde", | ||||||
|  |  "serde_json", | ||||||
|  |  "tokio", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "crossbeam-channel" | name = "crossbeam-channel" | ||||||
| version = "0.5.13" | version = "0.5.13" | ||||||
| @@ -1395,7 +1430,7 @@ dependencies = [ | |||||||
|  "ident_case", |  "ident_case", | ||||||
|  "proc-macro2", |  "proc-macro2", | ||||||
|  "quote", |  "quote", | ||||||
|  "strsim 0.11.1", |  "strsim", | ||||||
|  "syn 2.0.68", |  "syn 2.0.68", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| @@ -1841,6 +1876,15 @@ version = "1.0.7" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "foreign-types" | ||||||
|  | version = "0.3.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" | ||||||
|  | dependencies = [ | ||||||
|  |  "foreign-types-shared 0.1.1", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "foreign-types" | name = "foreign-types" | ||||||
| version = "0.5.0" | version = "0.5.0" | ||||||
| @@ -1848,7 +1892,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" | checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "foreign-types-macros", |  "foreign-types-macros", | ||||||
|  "foreign-types-shared", |  "foreign-types-shared 0.3.1", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| @@ -1862,6 +1906,12 @@ dependencies = [ | |||||||
|  "syn 2.0.68", |  "syn 2.0.68", | ||||||
| ] | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "foreign-types-shared" | ||||||
|  | version = "0.1.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "foreign-types-shared" | name = "foreign-types-shared" | ||||||
| version = "0.3.1" | version = "0.3.1" | ||||||
| @@ -2416,15 +2466,6 @@ version = "0.5.0" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" | 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]] | [[package]] | ||||||
| name = "hermit-abi" | name = "hermit-abi" | ||||||
| version = "0.3.9" | version = "0.3.9" | ||||||
| @@ -2747,6 +2788,12 @@ dependencies = [ | |||||||
|  "windows-sys 0.52.0", |  "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]] | [[package]] | ||||||
| name = "itoa" | name = "itoa" | ||||||
| version = "0.4.8" | version = "0.4.8" | ||||||
| @@ -3426,12 +3473,50 @@ version = "0.3.1" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" | checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "openssl" | ||||||
|  | version = "0.10.64" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" | ||||||
|  | dependencies = [ | ||||||
|  |  "bitflags 2.6.0", | ||||||
|  |  "cfg-if", | ||||||
|  |  "foreign-types 0.3.2", | ||||||
|  |  "libc", | ||||||
|  |  "once_cell", | ||||||
|  |  "openssl-macros", | ||||||
|  |  "openssl-sys", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "openssl-macros" | ||||||
|  | version = "0.1.1" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" | ||||||
|  | dependencies = [ | ||||||
|  |  "proc-macro2", | ||||||
|  |  "quote", | ||||||
|  |  "syn 2.0.68", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "openssl-probe" | name = "openssl-probe" | ||||||
| version = "0.1.5" | version = "0.1.5" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "openssl-sys" | ||||||
|  | version = "0.9.102" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" | ||||||
|  | dependencies = [ | ||||||
|  |  "cc", | ||||||
|  |  "libc", | ||||||
|  |  "pkg-config", | ||||||
|  |  "vcpkg", | ||||||
|  | ] | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "option-ext" | name = "option-ext" | ||||||
| version = "0.2.0" | version = "0.2.0" | ||||||
| @@ -3459,12 +3544,6 @@ dependencies = [ | |||||||
|  "windows-sys 0.52.0", |  "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]] | [[package]] | ||||||
| name = "outref" | name = "outref" | ||||||
| version = "0.5.1" | version = "0.5.1" | ||||||
| @@ -4562,9 +4641,9 @@ dependencies = [ | |||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "serde_json" | name = "serde_json" | ||||||
| version = "1.0.118" | version = "1.0.120" | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "d947f6b3163d8857ea16c4fa0dd4840d52f3041039a85decd46867eb1abef2e4" | checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "itoa 1.0.11", |  "itoa 1.0.11", | ||||||
|  "ryu", |  "ryu", | ||||||
| @@ -4788,7 +4867,7 @@ dependencies = [ | |||||||
|  "bytemuck", |  "bytemuck", | ||||||
|  "cfg_aliases", |  "cfg_aliases", | ||||||
|  "core-graphics", |  "core-graphics", | ||||||
|  "foreign-types", |  "foreign-types 0.5.0", | ||||||
|  "js-sys", |  "js-sys", | ||||||
|  "log", |  "log", | ||||||
|  "objc2", |  "objc2", | ||||||
| @@ -5181,12 +5260,6 @@ dependencies = [ | |||||||
|  "unicode-properties", |  "unicode-properties", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "strsim" |  | ||||||
| version = "0.10.0" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "strsim" | name = "strsim" | ||||||
| version = "0.11.1" | version = "0.11.1" | ||||||
| @@ -5549,21 +5622,6 @@ dependencies = [ | |||||||
|  "thiserror", |  "thiserror", | ||||||
| ] | ] | ||||||
|  |  | ||||||
| [[package]] |  | ||||||
| name = "tauri-plugin-single-instance" |  | ||||||
| version = "2.0.0-beta.9" |  | ||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" |  | ||||||
| checksum = "0ecafcc5214a5d3cd7a720c11e9c03cbd45ccaff721963485ec4ab481bdf4540" |  | ||||||
| dependencies = [ |  | ||||||
|  "log", |  | ||||||
|  "serde", |  | ||||||
|  "serde_json", |  | ||||||
|  "tauri", |  | ||||||
|  "thiserror", |  | ||||||
|  "windows-sys 0.52.0", |  | ||||||
|  "zbus", |  | ||||||
| ] |  | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "tauri-runtime" | name = "tauri-runtime" | ||||||
| version = "2.0.0-beta.18" | version = "2.0.0-beta.18" | ||||||
| @@ -5674,21 +5732,6 @@ dependencies = [ | |||||||
|  "utf-8", |  "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]] | [[package]] | ||||||
| name = "thin-slice" | name = "thin-slice" | ||||||
| version = "0.1.1" | version = "0.1.1" | ||||||
| @@ -6160,6 +6203,12 @@ version = "0.7.6" | |||||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
| checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" | ||||||
|  |  | ||||||
|  | [[package]] | ||||||
|  | name = "utf8parse" | ||||||
|  | version = "0.2.2" | ||||||
|  | source = "registry+https://github.com/rust-lang/crates.io-index" | ||||||
|  | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" | ||||||
|  |  | ||||||
| [[package]] | [[package]] | ||||||
| name = "uuid" | name = "uuid" | ||||||
| version = "1.9.1" | version = "1.9.1" | ||||||
| @@ -6953,15 +7002,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||||||
| checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030" | checksum = "7b8e3d6ae3342792a6cc2340e4394334c7402f3d793b390d2c5494a4032b3030" | ||||||
| dependencies = [ | dependencies = [ | ||||||
|  "async-broadcast", |  "async-broadcast", | ||||||
|  "async-executor", |  | ||||||
|  "async-fs", |  | ||||||
|  "async-io", |  | ||||||
|  "async-lock", |  | ||||||
|  "async-process", |  "async-process", | ||||||
|  "async-recursion", |  "async-recursion", | ||||||
|  "async-task", |  | ||||||
|  "async-trait", |  "async-trait", | ||||||
|  "blocking", |  | ||||||
|  "derivative", |  "derivative", | ||||||
|  "enumflags2", |  "enumflags2", | ||||||
|  "event-listener 5.3.1", |  "event-listener 5.3.1", | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| [package] | [package] | ||||||
| name = "creddy" | name = "creddy" | ||||||
| version = "0.4.9" | version = "0.6.1" | ||||||
| description = "A friendly AWS credentials manager" | description = "A friendly AWS credentials manager" | ||||||
| authors = ["Joseph Montanaro"] | authors = ["Joseph Montanaro"] | ||||||
| license = "" | license = "" | ||||||
| @@ -9,44 +9,46 @@ default-run = "creddy" | |||||||
| edition = "2021" | edition = "2021" | ||||||
| rust-version = "1.57" | rust-version = "1.57" | ||||||
|  |  | ||||||
| [[bin]] |  | ||||||
| name = "creddy_cli" |  | ||||||
| path = "src/bin/creddy_cli.rs" |  | ||||||
|  |  | ||||||
| [[bin]] | [[bin]] | ||||||
| name = "creddy" | name = "creddy" | ||||||
| path = "src/main.rs" | 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 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||||||
|  |  | ||||||
| [build-dependencies] | [build-dependencies] | ||||||
| tauri-build = { version = "2.0.0-beta", features = [] } | tauri-build = { version = "2.0.0-beta", features = [] } | ||||||
|  |  | ||||||
| [dependencies] | [dependencies] | ||||||
| serde_json = "1.0" | creddy_cli = { path = "./creddy_cli" } | ||||||
| serde = { version = "1.0", features = ["derive"] } |  | ||||||
| tauri = { version = "2.0.0-beta", features = ["tray-icon"] } | tauri = { version = "2.0.0-beta", features = ["tray-icon"] } | ||||||
| sodiumoxide = "0.2.7" | sodiumoxide = "0.2.7" | ||||||
| tokio = { version = ">=1.19", features = ["full"] } |  | ||||||
| sysinfo = "0.26.8" | sysinfo = "0.26.8" | ||||||
| aws-config = "1.5.3" | aws-config = "1.5.3" | ||||||
| aws-types = "1.3.2" | aws-types = "1.3.2" | ||||||
| aws-sdk-sts = "1.33.0" | aws-sdk-sts = "1.33.0" | ||||||
| aws-smithy-types = "1.2.0" | aws-smithy-types = "1.2.0" | ||||||
|  | dirs = { workspace = true } | ||||||
| thiserror = "1.0.38" | thiserror = "1.0.38" | ||||||
| once_cell = "1.16.0" | once_cell = "1.16.0" | ||||||
| strum = "0.24" | strum = "0.24" | ||||||
| strum_macros = "0.24" | strum_macros = "0.24" | ||||||
| auto-launch = "0.4.0" | auto-launch = "0.4.0" | ||||||
| dirs = "5.0" |  | ||||||
| clap = { version = "3.2.23", features = ["derive"] } |  | ||||||
| is-terminal = "0.4.7" | is-terminal = "0.4.7" | ||||||
| argon2 = { version = "0.5.0", features = ["std"] } | argon2 = { version = "0.5.0", features = ["std"] } | ||||||
| chacha20poly1305 = { version = "0.10.1", features = ["std"] } | chacha20poly1305 = { version = "0.10.1", features = ["std"] } | ||||||
| which = "4.4.0" | which = "4.4.0" | ||||||
| windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] } | windows = { version = "0.51.1", features = ["Win32_Foundation", "Win32_System_Pipes"] } | ||||||
| time = "0.3.31" | time = "0.3.31" | ||||||
| tauri-plugin-single-instance = "2.0.0-beta.9" |  | ||||||
| tauri-plugin-global-shortcut = "2.0.0-beta.6" | tauri-plugin-global-shortcut = "2.0.0-beta.6" | ||||||
| tauri-plugin-os = "2.0.0-beta.6" | tauri-plugin-os = "2.0.0-beta.6" | ||||||
| tauri-plugin-dialog = "2.0.0-beta.9" | tauri-plugin-dialog = "2.0.0-beta.9" | ||||||
| @@ -55,9 +57,16 @@ ssh-agent-lib = "0.4.0" | |||||||
| ssh-key = { version = "0.6.6", features = ["rsa", "ed25519", "encryption"] } | ssh-key = { version = "0.6.6", features = ["rsa", "ed25519", "encryption"] } | ||||||
| signature = "2.2.0" | signature = "2.2.0" | ||||||
| tokio-stream = "0.1.15" | tokio-stream = "0.1.15" | ||||||
|  | serde = { workspace = true } | ||||||
|  | serde_json = { workspace = true } | ||||||
| sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "uuid"] } | sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio", "uuid"] } | ||||||
|  | tokio = { workspace = true } | ||||||
| tokio-util = { version = "0.7.11", features = ["codec"] } | tokio-util = { version = "0.7.11", features = ["codec"] } | ||||||
| futures = "0.3.30" | futures = "0.3.30" | ||||||
|  | openssl = "0.10.64" | ||||||
|  | rsa = "0.9.6" | ||||||
|  | sha2 = "0.10.8" | ||||||
|  | ssh-encoding = "0.2.0" | ||||||
|  |  | ||||||
| [features] | [features] | ||||||
| # by default Tauri runs in production mode | # by default Tauri runs in production mode | ||||||
| @@ -67,8 +76,5 @@ default = ["custom-protocol"] | |||||||
| # DO NOT remove this | # DO NOT remove this | ||||||
| custom-protocol = ["tauri/custom-protocol"] | custom-protocol = ["tauri/custom-protocol"] | ||||||
|  |  | ||||||
| [dev-dependencies] |  | ||||||
| base64 = "0.22.1" |  | ||||||
|  |  | ||||||
| # [profile.dev.build-override] | # [profile.dev.build-override] | ||||||
| # opt-level = 3 | # 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.6.0" | ||||||
|  | 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 } | ||||||
							
								
								
									
										53
									
								
								src-tauri/creddy_cli/src/cli/docker.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src-tauri/creddy_cli/src/cli/docker.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | |||||||
|  | use std::io::{self, Read}; | ||||||
|  |  | ||||||
|  | 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())?; | ||||||
|  |  | ||||||
|  |     let req = CliRequest::StoreDockerCredential(input); | ||||||
|  |  | ||||||
|  |     match super::make_request(global_args.server_addr, &req)?? { | ||||||
|  |         CliResponse::Empty => Ok(()), | ||||||
|  |         r => bail!("Unexpected response from server: {r}"), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | pub fn docker_get(global_args: GlobalArgs) -> anyhow::Result<()> { | ||||||
|  |     let mut server_url = String::new(); | ||||||
|  |     io::stdin().read_to_string(&mut server_url)?; | ||||||
|  |     let req = CliRequest::GetDockerCredential { | ||||||
|  |        server_url: server_url.trim().to_owned() | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     match super::make_request(global_args.server_addr, &req)?? { | ||||||
|  |         CliResponse::Credential(CliCredential::Docker(d)) => { | ||||||
|  |             println!("{}", serde_json::to_string(&d)?); | ||||||
|  |         }, | ||||||
|  |         r => bail!("Unexpected response from server: {r}"), | ||||||
|  |     } | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | pub fn docker_erase(global_args: GlobalArgs) -> anyhow::Result<()> { | ||||||
|  |     let mut server_url = String::new(); | ||||||
|  |     io::stdin().read_to_string(&mut server_url)?; | ||||||
|  |     let req = CliRequest::EraseDockerCredential { | ||||||
|  |         server_url: server_url.trim().to_owned() | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     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)] | ||||||
|  |     pub 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::GetAwsCredential { | ||||||
|  |         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::GetAwsCredential { | ||||||
|  |         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{action: 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 => docker::docker_get(global_args), | ||||||
|  |         DockerCmd::Store => docker::docker_store(global_args), | ||||||
|  |         DockerCmd::Erase => docker::docker_erase(global_args), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | // 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) | ||||||
|  | } | ||||||
							
								
								
									
										59
									
								
								src-tauri/creddy_cli/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src-tauri/creddy_cli/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | |||||||
|  | mod cli; | ||||||
|  | pub use cli::{ | ||||||
|  |     Action, | ||||||
|  |     Cli, | ||||||
|  |     docker_credential_helper, | ||||||
|  |     exec, | ||||||
|  |     get, | ||||||
|  |     GlobalArgs, | ||||||
|  |     invoke_shortcut, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | pub(crate) use platform::connect; | ||||||
|  | pub use platform::server_addr; | ||||||
|  |  | ||||||
|  | pub mod proto; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | pub fn show_window(global_args: GlobalArgs) -> anyhow::Result<()> { | ||||||
|  |     let invoke = cli::InvokeArgs { shortcut_action: proto::ShortcutAction::ShowWindow }; | ||||||
|  |     cli::invoke_shortcut(invoke, global_args) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[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 default = if cfg!(debug_assertions) { "creddy-server-dev" } else { "creddy-server" }; | ||||||
|  |         let path = addr.unwrap_or_else(|| server_addr(default)); | ||||||
|  |         UnixStream::connect(&path).await | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn server_addr(sock_name: &str) -> PathBuf { | ||||||
|  |         let mut path = dirs::runtime_dir() | ||||||
|  |             .unwrap_or_else(|| PathBuf::from("/tmp")); | ||||||
|  |         if cfg!(debug_assertions) { | ||||||
|  |             path.push(format!("{sock_name}.dev.sock")) | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             path.push(format!("{sock_name}.sock")); | ||||||
|  |         } | ||||||
|  |         path | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[cfg(windows)] | ||||||
|  | mod platform { | ||||||
|  |     pub fn server_addr(sock_name: &str) -> String { | ||||||
|  |         if cfg!(debug_assertions) { | ||||||
|  |             format!(r"\\.\pipe\{sock_name}.dev") | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             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(()) | ||||||
|  | } | ||||||
							
								
								
									
										113
									
								
								src-tauri/creddy_cli/src/proto.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src-tauri/creddy_cli/src/proto.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | use std::fmt::{ | ||||||
|  |     Display, | ||||||
|  |     Formatter, | ||||||
|  |     Error as FmtError | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | use clap::ValueEnum; | ||||||
|  | use serde::{Serialize, Deserialize}; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | #[serde(tag = "type")] | ||||||
|  | pub enum CliRequest { | ||||||
|  |     GetAwsCredential { | ||||||
|  |         name: Option<String>, | ||||||
|  |         base: bool, | ||||||
|  |     }, | ||||||
|  |     GetDockerCredential { | ||||||
|  |         server_url: String, | ||||||
|  |     }, | ||||||
|  |     StoreDockerCredential(DockerCredential), | ||||||
|  |     EraseDockerCredential { | ||||||
|  |         server_url: String, | ||||||
|  |     }, | ||||||
|  |     InvokeShortcut{ | ||||||
|  |         action: 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 {} | ||||||
							
								
								
									
										12
									
								
								src-tauri/migrations/20240919135710_docker_creds.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src-tauri/migrations/20240919135710_docker_creds.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | 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, | ||||||
|  |     FOREIGN KEY(id) REFERENCES credentials(id) ON DELETE CASCADE | ||||||
|  | ); | ||||||
| @@ -15,13 +15,13 @@ use tauri::{ | |||||||
|     RunEvent, |     RunEvent, | ||||||
|     WindowEvent, |     WindowEvent, | ||||||
| }; | }; | ||||||
| use tauri::menu::MenuItem; | use creddy_cli::GlobalArgs; | ||||||
|  |  | ||||||
| use crate::{ | use crate::{ | ||||||
|     config::{self, AppConfig}, |     config::{self, AppConfig}, | ||||||
|     credentials::AppSession, |     credentials::AppSession, | ||||||
|     ipc, |     ipc, | ||||||
|     server::{Server, Agent}, |     srv::{creddy_server, agent}, | ||||||
|     errors::*, |     errors::*, | ||||||
|     shortcuts, |     shortcuts, | ||||||
|     state::AppState, |     state::AppState, | ||||||
| @@ -32,12 +32,13 @@ use crate::{ | |||||||
| pub static APP: OnceCell<AppHandle> = OnceCell::new(); | pub static APP: OnceCell<AppHandle> = OnceCell::new(); | ||||||
|  |  | ||||||
|  |  | ||||||
| pub fn run() -> tauri::Result<()> { | pub fn run(global_args: GlobalArgs) -> tauri::Result<()> { | ||||||
|  |     if let Ok(_) = creddy_cli::show_window(global_args) { | ||||||
|  |         // app is already running, so terminate | ||||||
|  |         return Ok(()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     tauri::Builder::default() |     tauri::Builder::default() | ||||||
|         .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { |  | ||||||
|             show_main_window(app) |  | ||||||
|                 .error_popup("Failed to show main window") |  | ||||||
|         })) |  | ||||||
|         .plugin(tauri_plugin_global_shortcut::Builder::default().build()) |         .plugin(tauri_plugin_global_shortcut::Builder::default().build()) | ||||||
|         .plugin(tauri_plugin_os::init()) |         .plugin(tauri_plugin_os::init()) | ||||||
|         .plugin(tauri_plugin_dialog::init()) |         .plugin(tauri_plugin_dialog::init()) | ||||||
| @@ -53,10 +54,12 @@ pub fn run() -> tauri::Result<()> { | |||||||
|             ipc::delete_credential, |             ipc::delete_credential, | ||||||
|             ipc::list_credentials, |             ipc::list_credentials, | ||||||
|             ipc::sshkey_from_file, |             ipc::sshkey_from_file, | ||||||
|  |             ipc::sshkey_from_private_key, | ||||||
|             ipc::get_config, |             ipc::get_config, | ||||||
|             ipc::save_config, |             ipc::save_config, | ||||||
|             ipc::launch_terminal, |             ipc::launch_terminal, | ||||||
|             ipc::get_setup_errors, |             ipc::get_setup_errors, | ||||||
|  |             ipc::get_devmode, | ||||||
|             ipc::exit, |             ipc::exit, | ||||||
|         ]) |         ]) | ||||||
|         .setup(|app| rt::block_on(setup(app))) |         .setup(|app| rt::block_on(setup(app))) | ||||||
| @@ -105,8 +108,8 @@ async fn setup(app: &mut App) -> Result<(), Box<dyn Error>> { | |||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     let app_session = AppSession::load(&pool).await?; |     let app_session = AppSession::load(&pool).await?; | ||||||
|     Server::start(app.handle().clone())?; |     creddy_server::serve(app.handle().clone())?; | ||||||
|     Agent::start(app.handle().clone())?; |     agent::serve(app.handle().clone())?; | ||||||
|  |  | ||||||
|     config::set_auto_launch(conf.start_on_login)?; |     config::set_auto_launch(conf.start_on_login)?; | ||||||
|     if let Err(_e) = config::set_auto_launch(conf.start_on_login) { |     if let Err(_e) = config::set_auto_launch(conf.start_on_login) { | ||||||
| @@ -157,8 +160,8 @@ fn start_auto_locker(app: AppHandle) { | |||||||
| pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> { | pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> { | ||||||
|     let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?; |     let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?; | ||||||
|     w.show()?; |     w.show()?; | ||||||
|     let show_hide = app.state::<MenuItem<tauri::Wry>>(); |     let menu = app.state::<tray::MenuItems>(); | ||||||
|     show_hide.set_text("Hide")?; |     menu.after_show()?; | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -166,8 +169,8 @@ pub fn show_main_window(app: &AppHandle) -> Result<(), WindowError> { | |||||||
| pub fn hide_main_window(app: &AppHandle) -> Result<(), WindowError> { | pub fn hide_main_window(app: &AppHandle) -> Result<(), WindowError> { | ||||||
|     let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?; |     let w = app.get_webview_window("main").ok_or(WindowError::NoMainWindow)?; | ||||||
|     w.hide()?; |     w.hide()?; | ||||||
|     let show_hide = app.state::<MenuItem<tauri::Wry>>(); |     let menu = app.state::<tray::MenuItems>(); | ||||||
|     show_hide.set_text("Show")?; |     menu.after_hide()?; | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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,194 +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::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 |  | ||||||
| } |  | ||||||
| @@ -5,7 +5,8 @@ use sysinfo::{ | |||||||
|     SystemExt, |     SystemExt, | ||||||
|     Pid, |     Pid, | ||||||
|     PidExt, |     PidExt, | ||||||
|     ProcessExt |     ProcessExt, | ||||||
|  |     UserExt, | ||||||
| }; | }; | ||||||
| use serde::{Serialize, Deserialize}; | use serde::{Serialize, Deserialize}; | ||||||
|  |  | ||||||
| @@ -16,6 +17,7 @@ use crate::errors::*; | |||||||
| pub struct Client { | pub struct Client { | ||||||
|     pub pid: u32, |     pub pid: u32, | ||||||
|     pub exe: Option<PathBuf>, |     pub exe: Option<PathBuf>, | ||||||
|  |     pub username: Option<String>, | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -23,6 +25,8 @@ pub fn get_client(pid: u32, parent: bool) -> Result<Client, ClientInfoError> { | |||||||
|     let sys_pid = Pid::from_u32(pid); |     let sys_pid = Pid::from_u32(pid); | ||||||
|     let mut sys = System::new(); |     let mut sys = System::new(); | ||||||
|     sys.refresh_process(sys_pid); |     sys.refresh_process(sys_pid); | ||||||
|  |     sys.refresh_users_list(); | ||||||
|  |  | ||||||
|     let mut proc = sys.process(sys_pid) |     let mut proc = sys.process(sys_pid) | ||||||
|         .ok_or(ClientInfoError::ProcessNotFound)?; |         .ok_or(ClientInfoError::ProcessNotFound)?; | ||||||
|  |  | ||||||
| @@ -34,10 +38,15 @@ pub fn get_client(pid: u32, parent: bool) -> Result<Client, ClientInfoError> { | |||||||
|             .ok_or(ClientInfoError::ParentProcessNotFound)?; |             .ok_or(ClientInfoError::ParentProcessNotFound)?; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     let username = proc.user_id() | ||||||
|  |         .map(|uid| sys.get_user_by_id(uid)) | ||||||
|  |         .flatten() | ||||||
|  |         .map(|u| u.name().to_owned()); | ||||||
|  |  | ||||||
|     let exe = match proc.exe() { |     let exe = match proc.exe() { | ||||||
|         p if p == Path::new("") => None, |         p if p == Path::new("") => None, | ||||||
|         p => Some(PathBuf::from(p)), |         p => Some(PathBuf::from(p)), | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     Ok(Client { pid: proc.pid().as_u32(), exe }) |     Ok(Client { pid: proc.pid().as_u32(), exe, username }) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -185,10 +185,16 @@ where S: Serializer | |||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use super::*; |     use super::*; | ||||||
|  |     use aws_sdk_sts::primitives::DateTimeFormat; | ||||||
|  |     use creddy_cli::proto::{ | ||||||
|  |         AwsBaseCredential as CliBase, | ||||||
|  |         AwsSessionCredential as CliSession, | ||||||
|  |     }; | ||||||
|     use sqlx::SqlitePool; |     use sqlx::SqlitePool; | ||||||
|     use sqlx::types::uuid::uuid; |     use sqlx::types::uuid::uuid; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     fn creds() -> AwsBaseCredential { |     fn creds() -> AwsBaseCredential { | ||||||
|         AwsBaseCredential::new( |         AwsBaseCredential::new( | ||||||
|             "AKIAIOSFODNN7EXAMPLE".into(), |             "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"))] |     #[sqlx::test(fixtures("aws_credentials"))] | ||||||
|     async fn test_load(pool: SqlitePool) { |     async fn test_load(pool: SqlitePool) { | ||||||
| @@ -255,4 +248,98 @@ mod tests { | |||||||
|         assert_eq!(&creds().into_credential(), &list[0]); |         assert_eq!(&creds().into_credential(), &list[0]); | ||||||
|         assert_eq!(&creds_2().into_credential(), &list[1]); |         assert_eq!(&creds_2().into_credential(), &list[1]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     // 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); | ||||||
|  |     } | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										196
									
								
								src-tauri/src/credentials/docker.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										196
									
								
								src-tauri/src/credentials/docker.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,196 @@ | |||||||
|  | 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) | INSERT INTO ssh_credentials (id, algorithm, comment, public_key, private_key_enc, nonce) | ||||||
| VALUES | VALUES | ||||||
|     ( |     ( | ||||||
|   | |||||||
| @@ -17,6 +17,9 @@ pub use aws::{AwsBaseCredential, AwsSessionCredential}; | |||||||
| mod crypto; | mod crypto; | ||||||
| pub use crypto::Crypto; | pub use crypto::Crypto; | ||||||
|  |  | ||||||
|  | mod docker; | ||||||
|  | pub use docker::DockerCredential; | ||||||
|  |  | ||||||
| mod record; | mod record; | ||||||
| pub use record::CredentialRecord; | pub use record::CredentialRecord; | ||||||
|  |  | ||||||
| @@ -32,6 +35,7 @@ pub use ssh::SshKey; | |||||||
| pub enum Credential { | pub enum Credential { | ||||||
|     AwsBase(AwsBaseCredential), |     AwsBase(AwsBaseCredential), | ||||||
|     AwsSession(AwsSessionCredential), |     AwsSession(AwsSessionCredential), | ||||||
|  |     Docker(DockerCredential), | ||||||
|     Ssh(SshKey), |     Ssh(SshKey), | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -79,6 +83,23 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized { | |||||||
|         Self::from_row(row, crypto) |         Self::from_row(row, crypto) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async fn load_by<T>(column: &str, value: T, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> | ||||||
|  |     where T: Send + for<'q> sqlx::Encode<'q, Sqlite> + sqlx::Type<Sqlite> | ||||||
|  |     { | ||||||
|  |         let query = format!( | ||||||
|  |             "SELECT * FROM {} where {} = ?", | ||||||
|  |             Self::table_name(), | ||||||
|  |             column, | ||||||
|  |         ); | ||||||
|  |         let row: Self::Row = sqlx::query_as(&query) | ||||||
|  |             .bind(value) | ||||||
|  |             .fetch_optional(pool) | ||||||
|  |             .await? | ||||||
|  |             .ok_or(LoadCredentialsError::NoCredentials)?; | ||||||
|  |  | ||||||
|  |         Self::from_row(row, crypto) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async fn load_default(crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> { |     async fn load_default(crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> { | ||||||
|         let q = format!( |         let q = format!( | ||||||
|             "SELECT details.* |             "SELECT details.* | ||||||
| @@ -118,3 +139,10 @@ pub trait PersistentCredential: for<'a> Deserialize<'a> + Sized { | |||||||
|         Ok(creds) |         Ok(creds) | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | pub fn random_uuid() -> Uuid { | ||||||
|  |     // a bit weird to use salt() for this, but it's convenient | ||||||
|  |     let random_bytes = Crypto::salt(); | ||||||
|  |     Uuid::from_slice(&random_bytes[..16]).unwrap() | ||||||
|  | } | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ use super::{ | |||||||
|     AwsBaseCredential, |     AwsBaseCredential, | ||||||
|     Credential, |     Credential, | ||||||
|     Crypto, |     Crypto, | ||||||
|  |     DockerCredential, | ||||||
|     PersistentCredential, |     PersistentCredential, | ||||||
|     SshKey, |     SshKey, | ||||||
| }; | }; | ||||||
| @@ -51,6 +52,7 @@ impl CredentialRecord { | |||||||
|         let type_name = match &self.credential { |         let type_name = match &self.credential { | ||||||
|             Credential::AwsBase(_) => AwsBaseCredential::type_name(), |             Credential::AwsBase(_) => AwsBaseCredential::type_name(), | ||||||
|             Credential::Ssh(_) => SshKey::type_name(), |             Credential::Ssh(_) => SshKey::type_name(), | ||||||
|  |             Credential::Docker(_) => DockerCredential::type_name(), | ||||||
|             _ => return Err(SaveCredentialsError::NotPersistent), |             _ => return Err(SaveCredentialsError::NotPersistent), | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
| @@ -86,6 +88,7 @@ impl CredentialRecord { | |||||||
|         match &self.credential { |         match &self.credential { | ||||||
|             Credential::AwsBase(b) => b.save_details(&self.id, crypto, &mut txn).await, |             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::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), |             _ => Err(SaveCredentialsError::NotPersistent), | ||||||
|         }?; |         }?; | ||||||
|  |  | ||||||
| @@ -112,15 +115,26 @@ impl CredentialRecord { | |||||||
|         Ok(Self::from_parts(row, credential)) |         Ok(Self::from_parts(row, credential)) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // pub async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> { |     #[cfg(test)] | ||||||
|     //     let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE id = ?") |     pub async fn load(id: &Uuid, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> { | ||||||
|     //         .bind(id) |         let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE id = ?") | ||||||
|     //         .fetch_optional(pool) |             .bind(id) | ||||||
|     //         .await? |             .fetch_optional(pool) | ||||||
|     //         .ok_or(LoadCredentialsError::NoCredentials)?; |             .await? | ||||||
|  |             .ok_or(LoadCredentialsError::NoCredentials)?; | ||||||
|  |  | ||||||
|     //     Self::load_credential(row, crypto, pool).await |         Self::load_credential(row, crypto, pool).await | ||||||
|     // } |     } | ||||||
|  |  | ||||||
|  |     pub async fn load_by_name(name: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> { | ||||||
|  |         let row: CredentialRow = sqlx::query_as("SELECT * FROM credentials WHERE name = ?") | ||||||
|  |             .bind(name) | ||||||
|  |             .fetch_optional(pool) | ||||||
|  |             .await? | ||||||
|  |             .ok_or(LoadCredentialsError::NoCredentials)?; | ||||||
|  |  | ||||||
|  |         Self::load_credential(row, crypto, pool).await | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub async fn load_default(credential_type: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> { |     pub async fn load_default(credential_type: &str, crypto: &Crypto, pool: &SqlitePool) -> Result<Self, LoadCredentialsError> { | ||||||
|         let row: CredentialRow = sqlx::query_as( |         let row: CredentialRow = sqlx::query_as( | ||||||
| @@ -156,6 +170,11 @@ impl CredentialRecord { | |||||||
|                 .ok_or(LoadCredentialsError::InvalidData)?; |                 .ok_or(LoadCredentialsError::InvalidData)?; | ||||||
|             records.push(Self::from_parts(parent, credential)); |             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) |         Ok(records) | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -12,6 +12,8 @@ use serde::ser::{ | |||||||
|     SerializeStruct, |     SerializeStruct, | ||||||
| }; | }; | ||||||
| use serde::de::{self, Visitor}; | use serde::de::{self, Visitor}; | ||||||
|  | use sha2::{Sha256, Sha512}; | ||||||
|  | use signature::{Signer, SignatureEncoding}; | ||||||
| use sqlx::{ | use sqlx::{ | ||||||
|     FromRow, |     FromRow, | ||||||
|     Sqlite, |     Sqlite, | ||||||
| @@ -19,11 +21,15 @@ use sqlx::{ | |||||||
|     Transaction, |     Transaction, | ||||||
|     types::Uuid, |     types::Uuid, | ||||||
| }; | }; | ||||||
| use ssh_agent_lib::proto::message::Identity; | use ssh_agent_lib::proto::message::{ | ||||||
|  |     Identity, | ||||||
|  |     SignRequest, | ||||||
|  | }; | ||||||
|  | use ssh_encoding::Encode; | ||||||
| use ssh_key::{ | use ssh_key::{ | ||||||
|     Algorithm, |     Algorithm, | ||||||
|     LineEnding, |     LineEnding, | ||||||
|     private::PrivateKey, |     private::{PrivateKey, KeypairData}, | ||||||
|     public::PublicKey, |     public::PublicKey, | ||||||
| }; | }; | ||||||
| use tokio_stream::StreamExt; | use tokio_stream::StreamExt; | ||||||
| @@ -74,6 +80,21 @@ impl SshKey { | |||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub fn from_private_key(private_key: &str, passphrase: &str) -> Result<SshKey, LoadSshKeyError> { | ||||||
|  |         let mut privkey = PrivateKey::from_openssh(private_key)?; | ||||||
|  |         if privkey.is_encrypted() { | ||||||
|  |             privkey = privkey.decrypt(passphrase) | ||||||
|  |                 .map_err(|_| LoadSshKeyError::InvalidPassphrase)?; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         Ok(SshKey { | ||||||
|  |             algorithm: privkey.algorithm(), | ||||||
|  |             comment: privkey.comment().into(), | ||||||
|  |             public_key: privkey.public_key().clone(), | ||||||
|  |             private_key: privkey, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub async fn name_from_pubkey(pubkey: &[u8], pool: &SqlitePool) -> Result<String, LoadCredentialsError> { |     pub async fn name_from_pubkey(pubkey: &[u8], pool: &SqlitePool) -> Result<String, LoadCredentialsError> { | ||||||
|         let row = sqlx::query!( |         let row = sqlx::query!( | ||||||
|             "SELECT c.name |             "SELECT c.name | ||||||
| @@ -104,6 +125,33 @@ impl SshKey { | |||||||
|  |  | ||||||
|         Ok(identities) |         Ok(identities) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub fn sign_request(&self, req: &SignRequest) -> Result<Vec<u8>, HandlerError> { | ||||||
|  |         let mut sig = Vec::new(); | ||||||
|  |         match self.private_key.key_data() { | ||||||
|  |             KeypairData::Rsa(keypair) => { | ||||||
|  |                 // 2 is the flag value for `SSH_AGENT_RSA_SHA2_256` | ||||||
|  |                 if req.flags & 2 > 0 { | ||||||
|  |                     let signer = rsa::pkcs1v15::SigningKey::<Sha256>::try_from(keypair)?; | ||||||
|  |                     let sig_data = signer.try_sign(&req.data)?.to_vec(); | ||||||
|  |                     "rsa-sha-256".encode(&mut sig)?; | ||||||
|  |                     sig_data.encode(&mut sig)?; | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     let signer = rsa::pkcs1v15::SigningKey::<Sha512>::try_from(keypair)?; | ||||||
|  |                     let sig_data = signer.try_sign(&req.data)?.to_vec(); | ||||||
|  |                     "rsa-sha2-512".encode(&mut sig)?; | ||||||
|  |                     sig_data.encode(&mut sig)?; | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             _ => { | ||||||
|  |                 let sig_data = self.private_key.try_sign(&req.data)?; | ||||||
|  |                 self.algorithm.as_str().encode(&mut sig)?; | ||||||
|  |                 sig_data.as_bytes().encode(&mut sig)?; | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |         Ok(sig) | ||||||
|  |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -250,8 +298,9 @@ fn deserialize_algorithm<'de, D>(deserializer: D) -> Result<Algorithm, D::Error> | |||||||
| #[cfg(test)] | #[cfg(test)] | ||||||
| mod tests { | mod tests { | ||||||
|     use std::fs::{self, File}; |     use std::fs::{self, File}; | ||||||
|     use ssh_key::Fingerprint; |  | ||||||
|     use sqlx::types::uuid::uuid; |     use sqlx::types::uuid::uuid; | ||||||
|  |     use crate::credentials::CredentialRecord; | ||||||
|  |  | ||||||
|     use super::*; |     use super::*; | ||||||
|  |  | ||||||
|     fn path(name: &str) -> String { |     fn path(name: &str) -> String { | ||||||
| @@ -387,11 +436,14 @@ mod tests { | |||||||
|     #[sqlx::test] |     #[sqlx::test] | ||||||
|     async fn test_save_db(pool: SqlitePool) { |     async fn test_save_db(pool: SqlitePool) { | ||||||
|         let crypto = Crypto::random(); |         let crypto = Crypto::random(); | ||||||
|         let k = rsa_plain(); |         let record = CredentialRecord { | ||||||
|         let mut txn = pool.begin().await.unwrap(); |             id: random_uuid(), | ||||||
|         k.save_details(&random_uuid(), &crypto, &mut txn).await |             name: "save_test".into(), | ||||||
|             .expect("Failed to save SSH key to database"); |             is_default: false, | ||||||
|         txn.commit().await.expect("Failed to finalize transaction"); |             credential: Credential::Ssh(rsa_plain()), | ||||||
|  |         }; | ||||||
|  |         record.save(&crypto, &pool).await | ||||||
|  |             .expect("Failed to save SSH key CredentialRecord to database"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -399,7 +451,7 @@ mod tests { | |||||||
|     async fn test_load_db(pool: SqlitePool) { |     async fn test_load_db(pool: SqlitePool) { | ||||||
|         let crypto = Crypto::fixed(); |         let crypto = Crypto::fixed(); | ||||||
|         let id = uuid!("11111111-1111-1111-1111-111111111111"); |         let id = uuid!("11111111-1111-1111-1111-111111111111"); | ||||||
|         let k = SshKey::load(&id, &crypto, &pool).await |         SshKey::load(&id, &crypto, &pool).await | ||||||
|             .expect("Failed to load SSH key from database"); |             .expect("Failed to load SSH key from database"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -407,13 +459,18 @@ mod tests { | |||||||
|     #[sqlx::test] |     #[sqlx::test] | ||||||
|     async fn test_save_load_db(pool: SqlitePool) { |     async fn test_save_load_db(pool: SqlitePool) { | ||||||
|         let crypto = Crypto::random(); |         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 loaded = SshKey::load(&id, &crypto, &pool).await.unwrap(); | ||||||
|  |         let known = ed25519_plain(); | ||||||
|  |  | ||||||
|         assert_eq!(known.algorithm, loaded.algorithm); |         assert_eq!(known.algorithm, loaded.algorithm); | ||||||
|         assert_eq!(known.comment, loaded.comment); |         assert_eq!(known.comment, loaded.comment); | ||||||
|   | |||||||
| @@ -173,7 +173,7 @@ pub enum HandlerError { | |||||||
|     StreamIOError(#[from] std::io::Error), |     StreamIOError(#[from] std::io::Error), | ||||||
|     #[error("Received invalid UTF-8 in request")] |     #[error("Received invalid UTF-8 in request")] | ||||||
|     InvalidUtf8(#[from] FromUtf8Error), |     InvalidUtf8(#[from] FromUtf8Error), | ||||||
|     #[error("HTTP request malformed")] |     #[error("Request malformed: {0}")] | ||||||
|     BadRequest(#[from] serde_json::Error), |     BadRequest(#[from] serde_json::Error), | ||||||
|     #[error("HTTP request too large")] |     #[error("HTTP request too large")] | ||||||
|     RequestTooLarge, |     RequestTooLarge, | ||||||
| @@ -183,6 +183,8 @@ pub enum HandlerError { | |||||||
|     Internal(#[from] RecvError), |     Internal(#[from] RecvError), | ||||||
|     #[error("Error accessing credentials: {0}")] |     #[error("Error accessing credentials: {0}")] | ||||||
|     NoCredentials(#[from] GetCredentialsError), |     NoCredentials(#[from] GetCredentialsError), | ||||||
|  |     #[error("Error saving credentials: {0}")] | ||||||
|  |     SaveCredentials(#[from] SaveCredentialsError), | ||||||
|     #[error("Error getting client details: {0}")] |     #[error("Error getting client details: {0}")] | ||||||
|     ClientInfo(#[from] ClientInfoError), |     ClientInfo(#[from] ClientInfoError), | ||||||
|     #[error("Error from Tauri: {0}")] |     #[error("Error from Tauri: {0}")] | ||||||
| @@ -195,6 +197,10 @@ pub enum HandlerError { | |||||||
|     SshAgent(#[from] ssh_agent_lib::error::AgentError), |     SshAgent(#[from] ssh_agent_lib::error::AgentError), | ||||||
|     #[error(transparent)] |     #[error(transparent)] | ||||||
|     SshKey(#[from] ssh_key::Error), |     SshKey(#[from] ssh_key::Error), | ||||||
|  |     #[error(transparent)] | ||||||
|  |     Signature(#[from] signature::Error), | ||||||
|  |     #[error(transparent)] | ||||||
|  |     Encoding(#[from] ssh_encoding::Error), | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -338,6 +344,8 @@ pub enum ClientInfoError { | |||||||
|     #[cfg(windows)] |     #[cfg(windows)] | ||||||
|     #[error("Could not determine PID of connected client")] |     #[error("Could not determine PID of connected client")] | ||||||
|     WindowsError(#[from] windows::core::Error), |     WindowsError(#[from] windows::core::Error), | ||||||
|  |     #[error("Could not determine PID of connected client")] | ||||||
|  |     PidNotFound, | ||||||
|     #[error(transparent)] |     #[error(transparent)] | ||||||
|     Io(#[from] std::io::Error), |     Io(#[from] std::io::Error), | ||||||
| } | } | ||||||
| @@ -364,7 +372,7 @@ pub enum RequestError { | |||||||
|     #[error("Error response from server: {0}")] |     #[error("Error response from server: {0}")] | ||||||
|     Server(ServerError), |     Server(ServerError), | ||||||
|     #[error("Unexpected response from server")] |     #[error("Unexpected response from server")] | ||||||
|     Unexpected(crate::server::Response), |     Unexpected(crate::srv::CliResponse), | ||||||
|     #[error("The server did not respond with valid JSON")] |     #[error("The server did not respond with valid JSON")] | ||||||
|     InvalidJson(#[from] serde_json::Error), |     InvalidJson(#[from] serde_json::Error), | ||||||
|     #[error("Error reading/writing stream: {0}")] |     #[error("Error reading/writing stream: {0}")] | ||||||
|   | |||||||
| @@ -14,37 +14,65 @@ use crate::state::AppState; | |||||||
| use crate::terminal; | use crate::terminal; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize)] | ||||||
|  | pub enum RequestAction { | ||||||
|  |     Access, | ||||||
|  |     Delete, | ||||||
|  |     Save, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| #[derive(Clone, Debug, Serialize, Deserialize)] | #[derive(Clone, Debug, Serialize, Deserialize)] | ||||||
| pub struct AwsRequestNotification { | pub struct AwsRequestNotification { | ||||||
|     pub id: u64, |  | ||||||
|     pub client: Client, |     pub client: Client, | ||||||
|  |     pub name: Option<String>, | ||||||
|     pub base: bool, |     pub base: bool, | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| #[derive(Clone, Debug, Serialize, Deserialize)] | #[derive(Clone, Debug, Serialize, Deserialize)] | ||||||
| pub struct SshRequestNotification { | pub struct SshRequestNotification { | ||||||
|     pub id: u64, |  | ||||||
|     pub client: Client, |     pub client: Client, | ||||||
|     pub key_name: String, |     pub key_name: String, | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize)] | ||||||
|  | pub struct DockerRequestNotification { | ||||||
|  |     pub action: RequestAction, | ||||||
|  |     pub client: Client, | ||||||
|  |     pub server_url: String, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| #[derive(Clone, Debug, Serialize, Deserialize)] | #[derive(Clone, Debug, Serialize, Deserialize)] | ||||||
| #[serde(tag = "type")] | #[serde(tag = "type")] | ||||||
| pub enum RequestNotification { | pub enum RequestNotificationDetail { | ||||||
|     Aws(AwsRequestNotification), |     Aws(AwsRequestNotification), | ||||||
|     Ssh(SshRequestNotification), |     Ssh(SshRequestNotification), | ||||||
|  |     Docker(DockerRequestNotification), | ||||||
| } | } | ||||||
|  |  | ||||||
| impl RequestNotification { | impl RequestNotificationDetail { | ||||||
|     pub fn new_aws(id: u64, client: Client, base: bool) -> Self { |     pub fn new_aws(client: Client, name: Option<String>, base: bool) -> Self { | ||||||
|         Self::Aws(AwsRequestNotification {id, client, base}) |         Self::Aws(AwsRequestNotification {client, name, base}) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub fn new_ssh(id: u64, client: Client, key_name: String) -> Self { |     pub fn new_ssh(client: Client, key_name: String) -> Self { | ||||||
|         Self::Ssh(SshRequestNotification {id, client, key_name}) |         Self::Ssh(SshRequestNotification {client, key_name}) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub fn new_docker(action: RequestAction, client: Client, server_url: String) -> Self { | ||||||
|  |         Self::Docker(DockerRequestNotification {action, client, server_url}) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[derive(Clone, Debug, Serialize, Deserialize)] | ||||||
|  | pub struct RequestNotification { | ||||||
|  |     pub id: u64, | ||||||
|  |     #[serde(flatten)] | ||||||
|  |     pub detail: RequestNotificationDetail, | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -141,6 +169,12 @@ pub async fn sshkey_from_file(path: &str, passphrase: &str) -> Result<SshKey, Lo | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[tauri::command] | ||||||
|  | pub async fn sshkey_from_private_key(private_key: &str, passphrase: &str) -> Result<SshKey, LoadSshKeyError> { | ||||||
|  |     SshKey::from_private_key(private_key, passphrase) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| #[tauri::command] | #[tauri::command] | ||||||
| pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> { | pub async fn get_config(app_state: State<'_, AppState>) -> Result<AppConfig, ()> { | ||||||
|     let config = app_state.config.read().await; |     let config = app_state.config.read().await; | ||||||
| @@ -170,6 +204,12 @@ pub async fn get_setup_errors(app_state: State<'_, AppState>) -> Result<Vec<Stri | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[tauri::command] | ||||||
|  | pub fn get_devmode() -> bool { | ||||||
|  |     cfg!(debug_assertions) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| #[tauri::command] | #[tauri::command] | ||||||
| pub fn exit(app_handle: AppHandle) { | pub fn exit(app_handle: AppHandle) { | ||||||
|     app_handle.exit(0) |     app_handle.exit(0) | ||||||
|   | |||||||
| @@ -44,12 +44,14 @@ pub async fn load_bytes(pool: &SqlitePool, name: &str) -> Result<Option<Vec<u8>> | |||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| // pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> { | // we don't have a need for this right now, but we will some day | ||||||
| //     sqlx::query!("DELETE FROM kv WHERE name = ?", name) | #[cfg(test)] | ||||||
| //         .execute(pool) | pub async fn delete(pool: &SqlitePool, name: &str) -> Result<(), sqlx::Error> { | ||||||
| //         .await?; |     sqlx::query!("DELETE FROM kv WHERE name = ?", name) | ||||||
| //     Ok(()) |         .execute(pool) | ||||||
| // } |         .await?; | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| pub async fn delete_multi(pool: &SqlitePool, names: &[&str]) -> Result<(), sqlx::Error> { | pub async fn delete_multi(pool: &SqlitePool, names: &[&str]) -> Result<(), sqlx::Error> { | ||||||
|   | |||||||
| @@ -1,5 +1,4 @@ | |||||||
| pub mod app; | pub mod app; | ||||||
| pub mod cli; |  | ||||||
| mod config; | mod config; | ||||||
| mod credentials; | mod credentials; | ||||||
| pub mod errors; | pub mod errors; | ||||||
| @@ -7,7 +6,7 @@ mod clientinfo; | |||||||
| mod ipc; | mod ipc; | ||||||
| mod kv; | mod kv; | ||||||
| mod state; | mod state; | ||||||
| pub mod server; | mod srv; | ||||||
| mod shortcuts; | mod shortcuts; | ||||||
| mod terminal; | mod terminal; | ||||||
| mod tray; | mod tray; | ||||||
|   | |||||||
| @@ -3,23 +3,25 @@ | |||||||
|     windows_subsystem = "windows" |     windows_subsystem = "windows" | ||||||
| )] | )] | ||||||
|  |  | ||||||
|  |  | ||||||
| use creddy::{ | use creddy::{ | ||||||
|     app, |     app, | ||||||
|     cli, |  | ||||||
|     errors::ShowError, |     errors::ShowError, | ||||||
| }; | }; | ||||||
|  | use creddy_cli::{Action, Cli}; | ||||||
|  |  | ||||||
|  |  | ||||||
| fn main() { | fn main() { | ||||||
|     let res = match cli::parser().get_matches().subcommand() { |     let cli = Cli::parse(); | ||||||
|         None | Some(("run", _)) => { |     let res = match cli.action { | ||||||
|             app::run().error_popup("Creddy encountered an error"); |         None | Some(Action::Run) => { | ||||||
|  |             app::run(cli.global_args).error_popup("Creddy encountered an error"); | ||||||
|             Ok(()) |             Ok(()) | ||||||
|         }, |         }, | ||||||
|         Some(("get", m)) => cli::get(m), |         Some(Action::Get(args)) => creddy_cli::get(args, cli.global_args), | ||||||
|         Some(("exec", m)) => cli::exec(m), |         Some(Action::Exec(args)) => creddy_cli::exec(args, cli.global_args), | ||||||
|         Some(("shortcut", m)) => cli::invoke_shortcut(m), |         Some(Action::Shortcut(args)) => creddy_cli::invoke_shortcut(args, cli.global_args), | ||||||
|         _ => unreachable!(), |         Some(Action::Docker(cmd)) => creddy_cli::docker_credential_helper(cmd, cli.global_args), | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     if let Err(e) = res { |     if let Err(e) = res { | ||||||
|   | |||||||
| @@ -1,77 +0,0 @@ | |||||||
| use signature::Signer; |  | ||||||
| use ssh_agent_lib::agent::{Agent, Session}; |  | ||||||
| use ssh_agent_lib::proto::message::Message; |  | ||||||
| use ssh_key::public::PublicKey; |  | ||||||
| use ssh_key::private::PrivateKey; |  | ||||||
| use tokio::net::UnixListener; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| struct SshAgent; |  | ||||||
|  |  | ||||||
| impl std::default::Default for SshAgent { |  | ||||||
|     fn default() -> Self { |  | ||||||
|         SshAgent {} |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #[ssh_agent_lib::async_trait] |  | ||||||
| impl Session for SshAgent { |  | ||||||
|     async fn handle(&mut self, message: Message) -> Result<Message, Box<dyn std::error::Error>> { |  | ||||||
|         println!("Received message"); |  | ||||||
|         match message { |  | ||||||
|             Message::RequestIdentities => { |  | ||||||
|                 let p = std::path::PathBuf::from("/home/joe/.ssh/id_ed25519.pub"); |  | ||||||
|                 let pubkey = PublicKey::read_openssh_file(&p).unwrap(); |  | ||||||
|                 let id = ssh_agent_lib::proto::message::Identity { |  | ||||||
|                     pubkey_blob: pubkey.to_bytes().unwrap(), |  | ||||||
|                     comment: pubkey.comment().to_owned(), |  | ||||||
|                 }; |  | ||||||
|                 Ok(Message::IdentitiesAnswer(vec![id])) |  | ||||||
|             }, |  | ||||||
|             Message::SignRequest(req) => { |  | ||||||
|                 println!("Received sign request"); |  | ||||||
|                 let mut req_bytes = vec![13]; |  | ||||||
|                 encode_string(&mut req_bytes, &req.pubkey_blob); |  | ||||||
|                 encode_string(&mut req_bytes, &req.data); |  | ||||||
|                 req_bytes.extend(req.flags.to_be_bytes()); |  | ||||||
|                 std::fs::File::create("/tmp/signreq").unwrap().write(&req_bytes).unwrap(); |  | ||||||
|  |  | ||||||
|                 let p = std::path::PathBuf::from("/home/joe/.ssh/id_ed25519"); |  | ||||||
|                 let passphrase = std::env::var("PRIVKEY_PASSPHRASE").unwrap(); |  | ||||||
|                 let privkey = PrivateKey::read_openssh_file(&p) |  | ||||||
|                     .unwrap() |  | ||||||
|                     .decrypt(passphrase.as_bytes()) |  | ||||||
|                     .unwrap(); |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|                 let sig = Signer::sign(&privkey, &req.data); |  | ||||||
|                 use std::io::Write; |  | ||||||
|                 std::fs::File::create("/tmp/sig").unwrap().write(sig.as_bytes()).unwrap(); |  | ||||||
|  |  | ||||||
|                 let mut payload = Vec::with_capacity(128); |  | ||||||
|                 encode_string(&mut payload, "ssh-ed25519".as_bytes()); |  | ||||||
|                 encode_string(&mut payload, sig.as_bytes()); |  | ||||||
|                 println!("Payload length: {}", payload.len()); |  | ||||||
|                 std::fs::File::create("/tmp/payload").unwrap().write(&payload).unwrap(); |  | ||||||
|                 Ok(Message::SignResponse(payload)) |  | ||||||
|             }, |  | ||||||
|             _ => Ok(Message::Failure), |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| fn encode_string(buf: &mut Vec<u8>, s: &[u8]) { |  | ||||||
|     let len = s.len() as u32; |  | ||||||
|     buf.extend(len.to_be_bytes()); |  | ||||||
|     buf.extend(s); |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| pub async fn run() { |  | ||||||
|     let socket = "/tmp/creddy-agent.sock"; |  | ||||||
|     let _ = std::fs::remove_file(socket); |  | ||||||
|     let listener = UnixListener::bind(socket).unwrap(); |  | ||||||
|     SshAgent.listen(listener).await.unwrap(); |  | ||||||
| } |  | ||||||
| @@ -1,171 +0,0 @@ | |||||||
| use tokio::io::{AsyncReadExt, AsyncWriteExt}; |  | ||||||
| use tokio::sync::oneshot; |  | ||||||
|  |  | ||||||
| use serde::{Serialize, Deserialize}; |  | ||||||
|  |  | ||||||
| use tauri::{AppHandle, Manager}; |  | ||||||
|  |  | ||||||
| use crate::errors::*; |  | ||||||
| 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; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| #[derive(Serialize, Deserialize)] |  | ||||||
| pub enum Request { |  | ||||||
|     GetAwsCredentials{  |  | ||||||
|         base: bool, |  | ||||||
|     }, |  | ||||||
|     InvokeShortcut(ShortcutAction), |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| #[derive(Debug, Serialize, Deserialize)] |  | ||||||
| pub enum Response { |  | ||||||
|     AwsBase(AwsBaseCredential), |  | ||||||
|     AwsSession(AwsSessionCredential), |  | ||||||
|     Empty, |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| struct CloseWaiter<'s> { |  | ||||||
|     stream: &'s mut Stream, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl<'s> CloseWaiter<'s> { |  | ||||||
|     async fn wait_for_close(&mut self) -> std::io::Result<()> { |  | ||||||
|         let mut buf = [0u8; 8]; |  | ||||||
|         loop { |  | ||||||
|             match self.stream.read(&mut buf).await { |  | ||||||
|                 Ok(0) => break Ok(()), |  | ||||||
|                 Ok(_) => (), |  | ||||||
|                 Err(e) => break Err(e), |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| 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; |  | ||||||
|     loop { |  | ||||||
|         n += stream.read_buf(&mut buf).await?; |  | ||||||
|         if let Some(&b'\n') = buf.last() { |  | ||||||
|             break; |  | ||||||
|         } |  | ||||||
|         else if n >= 1024 { |  | ||||||
|             return Err(HandlerError::RequestTooLarge); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let client = clientinfo::get_client(client_pid, true)?; |  | ||||||
|     let waiter = CloseWaiter { stream: &mut stream }; |  | ||||||
|  |  | ||||||
|     let req: Request = serde_json::from_slice(&buf)?; |  | ||||||
|     let res = match req { |  | ||||||
|         Request::GetAwsCredentials{ base } => get_aws_credentials( |  | ||||||
|             base, client, app_handle, waiter |  | ||||||
|         ).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 |  | ||||||
|     if let Err(HandlerError::Abandoned) = res { |  | ||||||
|         return Err(HandlerError::Abandoned); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let res = serde_json::to_vec(&res).unwrap(); |  | ||||||
|     stream.write_all(&res).await?; |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async fn invoke_shortcut(action: ShortcutAction) -> Result<Response, HandlerError> { |  | ||||||
|     shortcuts::exec_shortcut(action); |  | ||||||
|     Ok(Response::Empty) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async fn get_aws_credentials( |  | ||||||
|     base: bool, |  | ||||||
|     client: Client, |  | ||||||
|     app_handle: AppHandle, |  | ||||||
|     mut waiter: CloseWaiter<'_>, |  | ||||||
| ) -> Result<Response, HandlerError> { |  | ||||||
|     let state = app_handle.state::<AppState>(); |  | ||||||
|     let rehide_ms = { |  | ||||||
|         let config = state.config.read().await; |  | ||||||
|         config.rehide_ms |  | ||||||
|     }; |  | ||||||
|     let lease = state.acquire_visibility_lease(rehide_ms).await |  | ||||||
|         .map_err(|_e| HandlerError::NoMainWindow)?; // automate this conversion eventually? |  | ||||||
|  |  | ||||||
|     let (chan_send, chan_recv) = oneshot::channel(); |  | ||||||
|     let request_id = state.register_request(chan_send).await; |  | ||||||
|  |  | ||||||
|     // if an error occurs in any of the following, we want to abort the operation |  | ||||||
|     // 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, base); |  | ||||||
|         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); |  | ||||||
|             }, |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         match response.approval { |  | ||||||
|             Approval::Approved => { |  | ||||||
|                 if response.base { |  | ||||||
|                     let creds = state.get_aws_default().await?; |  | ||||||
|                     Ok(Response::AwsBase(creds)) |  | ||||||
|                 } |  | ||||||
|                 else { |  | ||||||
|                     let creds = state.get_aws_default_session().await?; |  | ||||||
|                     Ok(Response::AwsSession(creds.clone())) |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             Approval::Denied => Err(HandlerError::Denied), |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     let result = match proceed.await { |  | ||||||
|         Ok(r) => Ok(r), |  | ||||||
|         Err(e) => { |  | ||||||
|             state.unregister_request(request_id).await; |  | ||||||
|             Err(e) |  | ||||||
|         } |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     lease.release(); |  | ||||||
|     result |  | ||||||
| } |  | ||||||
| @@ -1,58 +0,0 @@ | |||||||
| use std::io::ErrorKind; |  | ||||||
| use tokio::net::{UnixListener, UnixStream}; |  | ||||||
| use tauri::{ |  | ||||||
|     AppHandle, |  | ||||||
|     async_runtime as rt, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| use crate::errors::*; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| pub type Stream = UnixStream; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| pub struct Server { |  | ||||||
|     listener: UnixListener, |  | ||||||
|     app_handle: AppHandle, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl Server { |  | ||||||
|     pub fn start(app_handle: AppHandle) -> std::io::Result<()> { |  | ||||||
|         match std::fs::remove_file("/tmp/creddy.sock") { |  | ||||||
|             Ok(_) => (), |  | ||||||
|             Err(e) if e.kind() == ErrorKind::NotFound => (), |  | ||||||
|             Err(e) => return Err(e), |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         let listener = UnixListener::bind("/tmp/creddy.sock")?; |  | ||||||
|         let srv = Server { listener, app_handle }; |  | ||||||
|         rt::spawn(srv.serve()); |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async fn serve(self) { |  | ||||||
|         loop { |  | ||||||
|             self.try_serve() |  | ||||||
|                 .await |  | ||||||
|                 .error_print_prefix("Error accepting request: "); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async fn try_serve(&self) -> Result<(), HandlerError> { |  | ||||||
|         let (stream, _addr) = self.listener.accept().await?; |  | ||||||
|         let new_handle = self.app_handle.clone(); |  | ||||||
|         let client_pid = get_client_pid(&stream)?; |  | ||||||
|         rt::spawn(async move { |  | ||||||
|             super::handle(stream, new_handle, client_pid) |  | ||||||
|                 .await |  | ||||||
|                 .error_print_prefix("Error responding to request: "); |  | ||||||
|         }); |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| fn get_client_pid(stream: &UnixStream) -> std::io::Result<u32> { |  | ||||||
|     let cred = stream.peer_cred()?; |  | ||||||
|     Ok(cred.pid().unwrap() as u32) |  | ||||||
| } |  | ||||||
| @@ -1,74 +0,0 @@ | |||||||
| use tokio::net::windows::named_pipe::{ |  | ||||||
|     NamedPipeServer, |  | ||||||
|     ServerOptions, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| use tauri::{AppHandle, Manager}; |  | ||||||
|  |  | ||||||
| use windows::Win32:: { |  | ||||||
|     Foundation::HANDLE, |  | ||||||
|     System::Pipes::GetNamedPipeClientProcessId, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| use std::os::windows::io::AsRawHandle; |  | ||||||
|  |  | ||||||
| use tauri::async_runtime as rt; |  | ||||||
|  |  | ||||||
| use crate::errors::*; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| // used by parent module |  | ||||||
| pub type Stream = NamedPipeServer; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| pub struct Server { |  | ||||||
|     listener: NamedPipeServer, |  | ||||||
|     app_handle: AppHandle, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl Server { |  | ||||||
|     pub fn start(app_handle: AppHandle) -> std::io::Result<()> { |  | ||||||
|         let listener = ServerOptions::new() |  | ||||||
|             .first_pipe_instance(true) |  | ||||||
|             .create(r"\\.\pipe\creddy-requests")?; |  | ||||||
|  |  | ||||||
|         let srv = Server {listener, app_handle}; |  | ||||||
|         rt::spawn(srv.serve()); |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async fn serve(mut self) { |  | ||||||
|         loop { |  | ||||||
|             if let Err(e) = self.try_serve().await { |  | ||||||
|                 eprintln!("Error accepting connection: {e}"); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async fn try_serve(&mut self) -> Result<(), HandlerError> { |  | ||||||
|         // connect() just waits for a client to connect, it doesn't return anything |  | ||||||
|         self.listener.connect().await?; |  | ||||||
|  |  | ||||||
|         // create a new pipe instance to listen for the next client, and swap it in |  | ||||||
|         let new_listener = ServerOptions::new().create(r"\\.\pipe\creddy-requests")?; |  | ||||||
|         let stream = std::mem::replace(&mut self.listener, new_listener); |  | ||||||
|         let new_handle = self.app_handle.clone(); |  | ||||||
|         let client_pid = get_client_pid(&stream)?; |  | ||||||
|         rt::spawn(async move { |  | ||||||
|             super::handle(stream, new_handle, client_pid) |  | ||||||
|                 .await |  | ||||||
|                 .error_print_prefix("Error responding to request: "); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| fn get_client_pid(pipe: &NamedPipeServer) -> Result<u32, ClientInfoError> { |  | ||||||
|     let raw_handle = pipe.as_raw_handle(); |  | ||||||
|     let mut pid = 0u32; |  | ||||||
|     let handle = HANDLE(raw_handle as _); |  | ||||||
|     unsafe { GetNamedPipeClientProcessId(handle, &mut pid as *mut u32)? }; |  | ||||||
|     Ok(pid) |  | ||||||
| } |  | ||||||
| @@ -1,151 +0,0 @@ | |||||||
| use std::io::ErrorKind; |  | ||||||
|  |  | ||||||
| use futures::SinkExt; |  | ||||||
| use signature::Signer; |  | ||||||
| use ssh_agent_lib::agent::MessageCodec; |  | ||||||
| use ssh_agent_lib::proto::message::{ |  | ||||||
|     Message, |  | ||||||
|     Identity, |  | ||||||
|     SignRequest, |  | ||||||
| }; |  | ||||||
| use tokio::net::{UnixListener, UnixStream}; |  | ||||||
| use tauri::{ |  | ||||||
|     AppHandle, |  | ||||||
|     Manager, |  | ||||||
|     async_runtime as rt, |  | ||||||
| }; |  | ||||||
| use tokio_util::codec::Framed; |  | ||||||
| use tokio_stream::StreamExt; |  | ||||||
| use tokio::sync::oneshot; |  | ||||||
|  |  | ||||||
| use crate::clientinfo; |  | ||||||
| use crate::errors::*; |  | ||||||
| use crate::ipc::{Approval, RequestNotification}; |  | ||||||
| use crate::state::AppState; |  | ||||||
|  |  | ||||||
|  |  | ||||||
| pub struct Agent { |  | ||||||
|     listener: UnixListener, |  | ||||||
|     app_handle: AppHandle, |  | ||||||
| } |  | ||||||
|  |  | ||||||
| impl Agent { |  | ||||||
|     pub fn start(app_handle: AppHandle) -> std::io::Result<()> { |  | ||||||
|         match std::fs::remove_file("/tmp/creddy-agent.sock") { |  | ||||||
|             Ok(_) => (), |  | ||||||
|             Err(e) if e.kind() == ErrorKind::NotFound => (), |  | ||||||
|             Err(e) => return Err(e), |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         let listener = UnixListener::bind("/tmp/creddy-agent.sock")?; |  | ||||||
|         let srv = Agent { listener, app_handle }; |  | ||||||
|         rt::spawn(srv.serve()); |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async fn serve(self) { |  | ||||||
|         loop { |  | ||||||
|             self.try_serve() |  | ||||||
|                 .await |  | ||||||
|                 .error_print_prefix("Error accepting request: "); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async fn try_serve(&self) -> Result<(), HandlerError> { |  | ||||||
|         let (stream, _addr) = self.listener.accept().await?; |  | ||||||
|         let new_handle = self.app_handle.clone(); |  | ||||||
|         let client_pid = get_client_pid(&stream)?; |  | ||||||
|         rt::spawn(async move { |  | ||||||
|             let adapter = Framed::new(stream, MessageCodec); |  | ||||||
|             handle_framed(adapter, new_handle, client_pid) |  | ||||||
|                 .await |  | ||||||
|                 .error_print_prefix("Error responding to request: "); |  | ||||||
|         }); |  | ||||||
|         Ok(()) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async fn handle_framed( |  | ||||||
|     mut adapter: Framed<UnixStream, MessageCodec>, |  | ||||||
|     app_handle: AppHandle, |  | ||||||
|     client_pid: u32, |  | ||||||
| ) -> Result<(), HandlerError> { |  | ||||||
|     while let Some(message) = adapter.try_next().await? { |  | ||||||
|         let resp = match message { |  | ||||||
|             Message::RequestIdentities => list_identities(app_handle.clone()).await?, |  | ||||||
|             Message::SignRequest(req) => sign_request(req, app_handle.clone(), client_pid).await?, |  | ||||||
|             _ => Message::Failure, |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         adapter.send(resp).await?; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     Ok(()) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async fn list_identities(app_handle: AppHandle) -> Result<Message, HandlerError> { |  | ||||||
|     let state = app_handle.state::<AppState>(); |  | ||||||
|     let identities: Vec<Identity> = state.list_ssh_identities().await?; |  | ||||||
|     Ok(Message::IdentitiesAnswer(identities)) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async fn sign_request(req: SignRequest, app_handle: AppHandle, client_pid: u32) -> Result<Message, HandlerError> { |  | ||||||
|     let state = app_handle.state::<AppState>(); |  | ||||||
|         let rehide_ms = { |  | ||||||
|         let config = state.config.read().await; |  | ||||||
|         config.rehide_ms |  | ||||||
|     }; |  | ||||||
|     let client = clientinfo::get_client(client_pid, false)?; |  | ||||||
|     let lease = state.acquire_visibility_lease(rehide_ms).await |  | ||||||
|         .map_err(|_e| HandlerError::NoMainWindow)?; |  | ||||||
|  |  | ||||||
|     let (chan_send, chan_recv) = oneshot::channel(); |  | ||||||
|     let request_id = state.register_request(chan_send).await; |  | ||||||
|  |  | ||||||
|     let proceed = async { |  | ||||||
|         let key_name = state.ssh_name_from_pubkey(&req.pubkey_blob).await?; |  | ||||||
|         let notification = RequestNotification::new_ssh(request_id, client, key_name.clone()); |  | ||||||
|         app_handle.emit("credential-request", ¬ification)?; |  | ||||||
|  |  | ||||||
|         let response = chan_recv.await?; |  | ||||||
|         if let Approval::Denied = response.approval { |  | ||||||
|             return Ok(Message::Failure); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         let key = state.sshkey_by_name(&key_name).await?; |  | ||||||
|         let sig = Signer::sign(&key.private_key, &req.data); |  | ||||||
|         let key_type = key.algorithm.as_str().as_bytes(); |  | ||||||
|  |  | ||||||
|         let payload_len = key_type.len() + sig.as_bytes().len() + 8; |  | ||||||
|         let mut payload = Vec::with_capacity(payload_len); |  | ||||||
|         encode_string(&mut payload, key.algorithm.as_str().as_bytes()); |  | ||||||
|         encode_string(&mut payload, sig.as_bytes()); |  | ||||||
|  |  | ||||||
|         Ok(Message::SignResponse(payload)) |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     let res = proceed.await; |  | ||||||
|     if let Err(_) = &res { |  | ||||||
|         state.unregister_request(request_id).await; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     lease.release(); |  | ||||||
|     res |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| fn get_client_pid(stream: &UnixStream) -> std::io::Result<u32> { |  | ||||||
|     let cred = stream.peer_cred()?; |  | ||||||
|     Ok(cred.pid().unwrap() as u32) |  | ||||||
| } |  | ||||||
|  |  | ||||||
|  |  | ||||||
| fn encode_string(buf: &mut Vec<u8>, s: &[u8]) { |  | ||||||
|     let len = s.len() as u32; |  | ||||||
|     buf.extend(len.to_be_bytes()); |  | ||||||
|     buf.extend(s); |  | ||||||
| } |  | ||||||
							
								
								
									
										88
									
								
								src-tauri/src/srv/agent.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src-tauri/src/srv/agent.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | |||||||
|  | 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_util::codec::Framed; | ||||||
|  |  | ||||||
|  | use crate::clientinfo; | ||||||
|  | use crate::errors::*; | ||||||
|  | use crate::ipc::{Approval, RequestNotificationDetail}; | ||||||
|  | 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, | ||||||
|  |     waiter: CloseWaiter<'_>, | ||||||
|  | ) -> Result<Message, HandlerError> { | ||||||
|  |     let state = app_handle.state::<AppState>(); | ||||||
|  |  | ||||||
|  |     let client = clientinfo::get_client(client_pid, false)?; | ||||||
|  |     let key_name = state.ssh_name_from_pubkey(&req.pubkey_blob).await?; | ||||||
|  |     let detail = RequestNotificationDetail::new_ssh(client, key_name.clone()); | ||||||
|  |  | ||||||
|  |     let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; | ||||||
|  |     match response.approval { | ||||||
|  |         Approval::Approved => { | ||||||
|  |             let key = state.sshkey_by_name(&key_name).await?; | ||||||
|  |             let sig = key.sign_request(&req)?; | ||||||
|  |             Ok(Message::SignResponse(sig)) | ||||||
|  |         }, | ||||||
|  |         Approval::Denied => Err(HandlerError::Abandoned), | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										223
									
								
								src-tauri/src/srv/creddy_server.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								src-tauri/src/srv/creddy_server.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,223 @@ | |||||||
|  | use tauri::{AppHandle, Manager}; | ||||||
|  | use tokio::io::{AsyncReadExt, AsyncWriteExt}; | ||||||
|  |  | ||||||
|  | use crate::clientinfo::{self, Client}; | ||||||
|  | use crate::credentials::{ | ||||||
|  |     self, | ||||||
|  |     Credential, | ||||||
|  |     CredentialRecord, | ||||||
|  |     DockerCredential, | ||||||
|  | }; | ||||||
|  | use crate::errors::*; | ||||||
|  | use crate::ipc::{ | ||||||
|  |     Approval, | ||||||
|  |     RequestAction, | ||||||
|  |     RequestNotificationDetail | ||||||
|  | }; | ||||||
|  | use crate::shortcuts::{self, ShortcutAction}; | ||||||
|  | use crate::state::AppState; | ||||||
|  | use super::{ | ||||||
|  |     CloseWaiter, | ||||||
|  |     CliCredential, | ||||||
|  |     CliRequest, | ||||||
|  |     CliResponse, | ||||||
|  |     Stream, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | pub fn serve(app_handle: AppHandle) -> std::io::Result<()> { | ||||||
|  |     super::serve("creddy-server", app_handle, handle) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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; | ||||||
|  |     loop { | ||||||
|  |         n += stream.read_buf(&mut buf).await?; | ||||||
|  |         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) { | ||||||
|  |             return Err(HandlerError::RequestTooLarge); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let client = clientinfo::get_client(client_pid, true)?; | ||||||
|  |     let waiter = CloseWaiter { stream: &mut stream }; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     let req: CliRequest = serde_json::from_slice(&buf)?; | ||||||
|  |     let res = match req { | ||||||
|  |         CliRequest::GetAwsCredential{ name, base } => get_aws_credentials( | ||||||
|  |             name, base, client, app_handle, waiter | ||||||
|  |         ).await, | ||||||
|  |         CliRequest::GetDockerCredential{ server_url } => get_docker_credential ( | ||||||
|  |             server_url, client, app_handle, waiter | ||||||
|  |         ).await, | ||||||
|  |         CliRequest::StoreDockerCredential(docker_credential) => store_docker_credential( | ||||||
|  |              docker_credential, app_handle, client, waiter | ||||||
|  |         ).await, | ||||||
|  |         CliRequest::EraseDockerCredential { server_url } => erase_docker_credential( | ||||||
|  |             server_url, app_handle, client, waiter | ||||||
|  |         ).await, | ||||||
|  |         CliRequest::InvokeShortcut{ action } => invoke_shortcut(action).await, | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     // doesn't make sense to send the error to the client if the client has already left | ||||||
|  |     if let Err(HandlerError::Abandoned) = res { | ||||||
|  |         return Err(HandlerError::Abandoned); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let res = serde_json::to_vec(&res).unwrap(); | ||||||
|  |     stream.write_all(&res).await?; | ||||||
|  |     Ok(()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async fn invoke_shortcut(action: ShortcutAction) -> Result<CliResponse, HandlerError> { | ||||||
|  |     shortcuts::exec_shortcut(action); | ||||||
|  |     Ok(CliResponse::Empty) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async fn get_aws_credentials( | ||||||
|  |     name: Option<String>, | ||||||
|  |     base: bool, | ||||||
|  |     client: Client, | ||||||
|  |     app_handle: AppHandle, | ||||||
|  |     waiter: CloseWaiter<'_>, | ||||||
|  | ) -> Result<CliResponse, HandlerError> { | ||||||
|  |     let detail = RequestNotificationDetail::new_aws(client, name.clone(), base); | ||||||
|  |     let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; | ||||||
|  |     match response.approval { | ||||||
|  |         Approval::Approved => { | ||||||
|  |             let state = app_handle.state::<AppState>(); | ||||||
|  |             if response.base { | ||||||
|  |                 let creds = state.get_aws_base(name).await?; | ||||||
|  |                 Ok(CliResponse::Credential(CliCredential::AwsBase(creds))) | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 let creds = state.get_aws_session(name).await?.clone(); | ||||||
|  |                 Ok(CliResponse::Credential(CliCredential::AwsSession(creds))) | ||||||
|  |             } | ||||||
|  |         }, | ||||||
|  |         Approval::Denied => Err(HandlerError::Denied), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn get_docker_credential( | ||||||
|  |     server_url: String, | ||||||
|  |     client: Client, | ||||||
|  |     app_handle: AppHandle, | ||||||
|  |     waiter: CloseWaiter<'_>, | ||||||
|  | ) -> Result<CliResponse, HandlerError> { | ||||||
|  |     let state = app_handle.state::<AppState>(); | ||||||
|  |     let meta = state.docker_credential_meta(&server_url).await.unwrap_or(None); | ||||||
|  |     if  meta.is_none() { | ||||||
|  |         return Err( | ||||||
|  |             HandlerError::NoCredentials( | ||||||
|  |                 GetCredentialsError::Load( | ||||||
|  |                     LoadCredentialsError::NoCredentials | ||||||
|  |                 ) | ||||||
|  |             ) | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let detail = RequestNotificationDetail::new_docker( | ||||||
|  |         RequestAction::Access, | ||||||
|  |         client, | ||||||
|  |         server_url.clone() | ||||||
|  |     ); | ||||||
|  |     let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; | ||||||
|  |     match response.approval { | ||||||
|  |         Approval::Approved => { | ||||||
|  |             let creds = state.get_docker_credential(&server_url).await?; | ||||||
|  |             Ok(CliResponse::Credential(CliCredential::Docker(creds))) | ||||||
|  |         }, | ||||||
|  |         Approval::Denied => { | ||||||
|  |             Err(HandlerError::Denied) | ||||||
|  |         }, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn store_docker_credential( | ||||||
|  |     docker_credential: DockerCredential, | ||||||
|  |     app_handle: AppHandle, | ||||||
|  |     client: Client, | ||||||
|  |     waiter: CloseWaiter<'_>, | ||||||
|  | ) -> Result<CliResponse, HandlerError> { | ||||||
|  |     let state = app_handle.state::<AppState>(); | ||||||
|  |  | ||||||
|  |     // We want to do this before asking for confirmation from the user, because Docker has an annoying | ||||||
|  |     // habit of calling `get` and then immediately turning around and calling `store` with the same | ||||||
|  |     // data. In that case we want to avoid asking for confirmation at all. | ||||||
|  |     match state.get_docker_credential(&docker_credential.server_url).await { | ||||||
|  |         // if there is already a credential with this server_url, and it is unchanged, we're done | ||||||
|  |         Ok(c) if c == docker_credential => return Ok(CliResponse::Empty), | ||||||
|  |         // otherwise we are making an update, so proceed | ||||||
|  |         Ok(_) => (), | ||||||
|  |         // if the app is locked, then this isn't the situation described above, so proceed | ||||||
|  |         Err(GetCredentialsError::Locked) => (), | ||||||
|  |         // if the app is unlocked, and there is no matching credential, proceed | ||||||
|  |         Err(GetCredentialsError::Load(LoadCredentialsError::NoCredentials)) => (), | ||||||
|  |         // any other error is a failure | ||||||
|  |         Err(e) => return Err(e.into()), | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let detail = RequestNotificationDetail::new_docker( | ||||||
|  |         RequestAction::Save, | ||||||
|  |         client, | ||||||
|  |         docker_credential.server_url.clone(), | ||||||
|  |     ); | ||||||
|  |     let response = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; | ||||||
|  |     if matches!(response.approval, Approval::Denied) { | ||||||
|  |         return Err(HandlerError::Denied); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let (id, name) = state.docker_credential_meta(&docker_credential.server_url) | ||||||
|  |         .await | ||||||
|  |         .map_err(|e| GetCredentialsError::Load(e))? | ||||||
|  |         .unwrap_or_else(|| (credentials::random_uuid(), docker_credential.server_url.clone())); | ||||||
|  |  | ||||||
|  |     let record = CredentialRecord { | ||||||
|  |         id, | ||||||
|  |         name, | ||||||
|  |         is_default: false, | ||||||
|  |         credential: Credential::Docker(docker_credential) | ||||||
|  |     }; | ||||||
|  |     state.save_credential(record).await?; | ||||||
|  |  | ||||||
|  |     Ok(CliResponse::Empty) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async fn erase_docker_credential( | ||||||
|  |     server_url: String, | ||||||
|  |     app_handle: AppHandle, | ||||||
|  |     client: Client, | ||||||
|  |     waiter: CloseWaiter<'_> | ||||||
|  | ) -> Result<CliResponse, HandlerError> { | ||||||
|  |     let state = app_handle.state::<AppState>(); | ||||||
|  |  | ||||||
|  |     let detail = RequestNotificationDetail::new_docker( | ||||||
|  |         RequestAction::Delete, | ||||||
|  |         client, | ||||||
|  |         server_url.clone(), | ||||||
|  |     ); | ||||||
|  |     let resp = super::send_credentials_request(detail, app_handle.clone(), waiter).await?; | ||||||
|  |     match resp.approval { | ||||||
|  |         Approval::Approved => { | ||||||
|  |             state.delete_credential_by_name(&server_url).await?; | ||||||
|  |             Ok(CliResponse::Empty) | ||||||
|  |         } | ||||||
|  |         Approval::Denied => { | ||||||
|  |             Err(HandlerError::Denied) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										225
									
								
								src-tauri/src/srv/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								src-tauri/src/srv/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,225 @@ | |||||||
|  | use std::future::Future; | ||||||
|  |  | ||||||
|  | use tauri::{ | ||||||
|  |     AppHandle, | ||||||
|  |     async_runtime as rt, | ||||||
|  |     Manager, | ||||||
|  | }; | ||||||
|  | use tokio::io::AsyncReadExt; | ||||||
|  | use tokio::sync::oneshot; | ||||||
|  | use serde::{Serialize, Deserialize}; | ||||||
|  |  | ||||||
|  | use crate::credentials::{ | ||||||
|  |     AwsBaseCredential, | ||||||
|  |     AwsSessionCredential, | ||||||
|  |     DockerCredential, | ||||||
|  | }; | ||||||
|  | use crate::errors::*; | ||||||
|  | use crate::ipc::{RequestNotification, RequestNotificationDetail, RequestResponse}; | ||||||
|  | use crate::shortcuts::ShortcutAction; | ||||||
|  | use crate::state::AppState; | ||||||
|  |  | ||||||
|  | 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)] | ||||||
|  | #[serde(tag = "type")] | ||||||
|  | pub enum CliRequest { | ||||||
|  |     GetAwsCredential { | ||||||
|  |         name: Option<String>, | ||||||
|  |         base: bool, | ||||||
|  |     }, | ||||||
|  |     GetDockerCredential { | ||||||
|  |         server_url: String, | ||||||
|  |     }, | ||||||
|  |     StoreDockerCredential(DockerCredential), | ||||||
|  |     EraseDockerCredential { | ||||||
|  |         server_url: String, | ||||||
|  |     }, | ||||||
|  |     InvokeShortcut{ | ||||||
|  |         action: ShortcutAction, | ||||||
|  |     }, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | pub enum CliResponse { | ||||||
|  |     Credential(CliCredential), | ||||||
|  |     Empty, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[derive(Debug, Serialize, Deserialize)] | ||||||
|  | pub enum CliCredential { | ||||||
|  |     AwsBase(AwsBaseCredential), | ||||||
|  |     AwsSession(AwsSessionCredential), | ||||||
|  |     Docker(DockerCredential), | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | 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(()) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async fn send_credentials_request( | ||||||
|  |     detail: RequestNotificationDetail, | ||||||
|  |     app_handle: AppHandle, | ||||||
|  |     mut waiter: CloseWaiter<'_> | ||||||
|  | ) -> Result<RequestResponse, HandlerError> { | ||||||
|  |     let state = app_handle.state::<AppState>(); | ||||||
|  |     let rehide_ms = { | ||||||
|  |         let config = state.config.read().await; | ||||||
|  |         config.rehide_ms | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     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 notification = RequestNotification { id: request_id, detail }; | ||||||
|  |  | ||||||
|  |     // the following could fail in various ways, but we want to make sure | ||||||
|  |     // the request gets unregistered on any failure, so we wrap this all | ||||||
|  |     // up in an async block so that we only have to handle the error case once | ||||||
|  |     let proceed = async { | ||||||
|  |         app_handle.emit("credential-request", ¬ification)?; | ||||||
|  |         tokio::select! { | ||||||
|  |             r = chan_recv => Ok(r?), | ||||||
|  |             _ = waiter.wait_for_close() => { | ||||||
|  |                 app_handle.emit("request-cancelled", request_id)?; | ||||||
|  |                 Err(HandlerError::Abandoned) | ||||||
|  |             }, | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     let res = proceed.await; | ||||||
|  |     if let Err(_) = &res { | ||||||
|  |         state.unregister_request(request_id).await; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     lease.release(); | ||||||
|  |     res | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | #[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)) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -19,6 +19,7 @@ use crate::app; | |||||||
| use crate::credentials::{ | use crate::credentials::{ | ||||||
|     AppSession, |     AppSession, | ||||||
|     AwsSessionCredential, |     AwsSessionCredential, | ||||||
|  |     DockerCredential, | ||||||
|     SshKey, |     SshKey, | ||||||
| }; | }; | ||||||
| use crate::{config, config::AppConfig}; | use crate::{config, config::AppConfig}; | ||||||
| @@ -31,6 +32,7 @@ use crate::credentials::{ | |||||||
| use crate::ipc::{self, RequestResponse}; | use crate::ipc::{self, RequestResponse}; | ||||||
| use crate::errors::*; | use crate::errors::*; | ||||||
| use crate::shortcuts; | use crate::shortcuts; | ||||||
|  | use crate::tray; | ||||||
|  |  | ||||||
|  |  | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| @@ -160,6 +162,13 @@ impl AppState { | |||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub async fn delete_credential_by_name(&self, name: &str) -> Result<(), SaveCredentialsError> { | ||||||
|  |         sqlx::query!("DELETE FROM credentials WHERE name = ?", name) | ||||||
|  |             .execute(&self.pool) | ||||||
|  |             .await?; | ||||||
|  |         Ok(()) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub async fn list_credentials(&self) -> Result<Vec<CredentialRecord>, GetCredentialsError> { |     pub async fn list_credentials(&self) -> Result<Vec<CredentialRecord>, GetCredentialsError> { | ||||||
|         let session = self.app_session.read().await; |         let session = self.app_session.read().await; | ||||||
|         let crypto = session.try_get_crypto()?; |         let crypto = session.try_get_crypto()?; | ||||||
| @@ -244,7 +253,11 @@ impl AppState { | |||||||
|  |  | ||||||
|     pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> { |     pub async fn unlock(&self, passphrase: &str) -> Result<(), UnlockError> { | ||||||
|         let mut session = self.app_session.write().await; |         let mut session = self.app_session.write().await; | ||||||
|         session.unlock(passphrase) |         session.unlock(passphrase)?; | ||||||
|  |         let app_handle = app::APP.get().unwrap(); | ||||||
|  |         let menu = app_handle.state::<tray::MenuItems>(); | ||||||
|  |         let _ = menu.after_unlock(); // we don't care if this fails, it's non-essential | ||||||
|  |         Ok(()) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub async fn lock(&self) -> Result<(), LockError> { |     pub async fn lock(&self) -> Result<(), LockError> { | ||||||
| @@ -258,6 +271,9 @@ impl AppState { | |||||||
|                 let app_handle = app::APP.get().unwrap(); |                 let app_handle = app::APP.get().unwrap(); | ||||||
|                 app_handle.emit("locked", None::<usize>)?; |                 app_handle.emit("locked", None::<usize>)?; | ||||||
|  |  | ||||||
|  |                 let menu = app_handle.state::<tray::MenuItems>(); | ||||||
|  |                 let _ = menu.after_lock(); | ||||||
|  |  | ||||||
|                 Ok(()) |                 Ok(()) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -270,22 +286,23 @@ impl AppState { | |||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub async fn get_aws_default(&self) -> Result<AwsBaseCredential, GetCredentialsError> { |     pub async fn get_aws_base(&self, name: Option<String>) -> Result<AwsBaseCredential, GetCredentialsError> { | ||||||
|         let app_session = self.app_session.read().await; |         let app_session = self.app_session.read().await; | ||||||
|         let crypto = app_session.try_get_crypto()?; |         let crypto = app_session.try_get_crypto()?; | ||||||
|         let creds = AwsBaseCredential::load_default(crypto, &self.pool).await?; |         let creds = match name { | ||||||
|         // let record = CredentialRecord::load_default("aws", crypto, &self.pool).await?; |             Some(n) => AwsBaseCredential::load_by_name(&n, crypto, &self.pool).await?, | ||||||
|         // let creds = match record.credential { |             None => AwsBaseCredential::load_default(crypto, &self.pool).await?, | ||||||
|         //     Credential::AwsBase(b) => Ok(b), |         }; | ||||||
|         //     _ => Err(LoadCredentialsError::NoCredentials) |  | ||||||
|         // }?; |  | ||||||
|         Ok(creds) |         Ok(creds) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     pub async fn get_aws_default_session(&self) -> Result<RwLockReadGuard<'_, AwsSessionCredential>, GetCredentialsError> { |     pub async fn get_aws_session(&self, name: Option<String>) -> Result<RwLockReadGuard<'_, AwsSessionCredential>, GetCredentialsError> { | ||||||
|         let app_session = self.app_session.read().await; |         let app_session = self.app_session.read().await; | ||||||
|         let crypto = app_session.try_get_crypto()?; |         let crypto = app_session.try_get_crypto()?; | ||||||
|         let record = CredentialRecord::load_default("aws", crypto, &self.pool).await?; |         let record = match name { | ||||||
|  |             Some(n) => CredentialRecord::load_by_name(&n, crypto, &self.pool).await?, | ||||||
|  |             None => CredentialRecord::load_default("aws", crypto, &self.pool).await?, | ||||||
|  |         }; | ||||||
|         let base = match &record.credential { |         let base = match &record.credential { | ||||||
|             Credential::AwsBase(b) => Ok(b), |             Credential::AwsBase(b) => Ok(b), | ||||||
|             _ => Err(LoadCredentialsError::NoCredentials) |             _ => Err(LoadCredentialsError::NoCredentials) | ||||||
| @@ -321,6 +338,30 @@ impl AppState { | |||||||
|         Ok(k) |         Ok(k) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     pub async fn docker_credential_meta( | ||||||
|  |         &self, server_url: &str | ||||||
|  |     ) -> Result<Option<(Uuid, String)>, LoadCredentialsError> { | ||||||
|  |         let res = sqlx::query!( | ||||||
|  |             r#"SELECT | ||||||
|  |                 c.id as "id: Uuid", | ||||||
|  |                 c.name | ||||||
|  |             FROM | ||||||
|  |                 credentials c | ||||||
|  |                 JOIN docker_credentials d | ||||||
|  |                     ON d.id = c.id | ||||||
|  |             WHERE d.server_url = ?"#, | ||||||
|  |             server_url | ||||||
|  |         ).fetch_optional(&self.pool).await?; | ||||||
|  |         Ok(res.map(|row| (row.id, row.name))) | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub async fn get_docker_credential(&self, server_url: &str) -> Result<DockerCredential, GetCredentialsError> { | ||||||
|  |         let app_session = self.app_session.read().await; | ||||||
|  |         let crypto = app_session.try_get_crypto()?; | ||||||
|  |         let d = DockerCredential::load_by("server_url", server_url.to_owned(), crypto, &self.pool).await?; | ||||||
|  |         Ok(d) | ||||||
|  |     } | ||||||
|  |  | ||||||
|     pub async fn signal_activity(&self) { |     pub async fn signal_activity(&self) { | ||||||
|         let mut last_activity = self.last_activity.write().await; |         let mut last_activity = self.last_activity.write().await; | ||||||
|         *last_activity = OffsetDateTime::now_utc(); |         *last_activity = OffsetDateTime::now_utc(); | ||||||
|   | |||||||
| @@ -63,12 +63,12 @@ async fn do_launch(app: &AppHandle, use_base: bool) -> Result<(), LaunchTerminal | |||||||
|     // (i.e. lies about unlocking) we could end up here with a locked session |     // (i.e. lies about unlocking) we could end up here with a locked session | ||||||
|     // this will result in an error popup to the user (see main hotkey handler) |     // this will result in an error popup to the user (see main hotkey handler) | ||||||
|     if use_base { |     if use_base { | ||||||
|         let base_creds = state.get_aws_default().await?; |         let base_creds = state.get_aws_base(None).await?; | ||||||
|         cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id); |         cmd.env("AWS_ACCESS_KEY_ID", &base_creds.access_key_id); | ||||||
|         cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key); |         cmd.env("AWS_SECRET_ACCESS_KEY", &base_creds.secret_access_key); | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         let session_creds = state.get_aws_default_session().await?; |         let session_creds = state.get_aws_session(None).await?; | ||||||
|         cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id); |         cmd.env("AWS_ACCESS_KEY_ID", &session_creds.access_key_id); | ||||||
|         cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key); |         cmd.env("AWS_SECRET_ACCESS_KEY", &session_creds.secret_access_key); | ||||||
|         cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token); |         cmd.env("AWS_SESSION_TOKEN", &session_creds.session_token); | ||||||
|   | |||||||
| @@ -7,27 +7,74 @@ use tauri::{ | |||||||
| use tauri::menu::{ | use tauri::menu::{ | ||||||
|     MenuBuilder, |     MenuBuilder, | ||||||
|     MenuEvent, |     MenuEvent, | ||||||
|  |     MenuItem, | ||||||
|     MenuItemBuilder, |     MenuItemBuilder, | ||||||
|  |     PredefinedMenuItem, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| use crate::app; | use crate::app; | ||||||
| use crate::state::AppState; | use crate::state::AppState; | ||||||
|  |  | ||||||
|  |  | ||||||
|  | pub struct MenuItems { | ||||||
|  |     pub status: MenuItem<tauri::Wry>, | ||||||
|  |     pub show_hide: MenuItem<tauri::Wry>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | impl MenuItems { | ||||||
|  |     pub fn after_show(&self) -> tauri::Result<()> { | ||||||
|  |         self.show_hide.set_text("Hide") | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn after_hide(&self) -> tauri::Result<()> { | ||||||
|  |         self.show_hide.set_text("Show") | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn after_lock(&self) -> tauri::Result<()> { | ||||||
|  |         if cfg!(debug_assertions) { | ||||||
|  |             self.status.set_text("Creddy (dev): Locked") | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             self.status.set_text("Creddy: Locked") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     pub fn after_unlock(&self) -> tauri::Result<()> { | ||||||
|  |         if cfg!(debug_assertions) { | ||||||
|  |             self.status.set_text("Creddy (dev): Unlocked") | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             self.status.set_text("Creddy: Unlocked") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
| pub fn setup(app: &App) -> tauri::Result<()> { | pub fn setup(app: &App) -> tauri::Result<()> { | ||||||
|  |     let status_text = | ||||||
|  |         if cfg!(debug_assertions) { | ||||||
|  |             "Creddy (dev): Locked" | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             "Creddy: Locked" | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |     let status = MenuItemBuilder::with_id("status", status_text) | ||||||
|  |         .enabled(false) | ||||||
|  |         .build(app)?; | ||||||
|  |     let sep = PredefinedMenuItem::separator(app)?; | ||||||
|     let show_hide = MenuItemBuilder::with_id("show_hide", "Show").build(app)?; |     let show_hide = MenuItemBuilder::with_id("show_hide", "Show").build(app)?; | ||||||
|     let exit = MenuItemBuilder::with_id("exit", "Exit").build(app)?; |     let exit = MenuItemBuilder::with_id("exit", "Exit").build(app)?; | ||||||
|  |  | ||||||
|     let menu = MenuBuilder::new(app) |     let menu = MenuBuilder::new(app) | ||||||
|         .items(&[&show_hide, &exit]) |         .items(&[&status, &sep, &show_hide, &exit]); | ||||||
|         .build()?; |  | ||||||
|  |  | ||||||
|     let tray = app.tray_by_id("main").unwrap(); |     let tray = app.tray_by_id("main").unwrap(); | ||||||
|     tray.set_menu(Some(menu))?; |     tray.set_menu(Some(menu.build()?))?; | ||||||
|     tray.on_menu_event(handle_event); |     tray.on_menu_event(handle_event); | ||||||
|  |  | ||||||
|     // stash this so we can find it later to change the text |     // stash these so we can find them later to change the text | ||||||
|     app.manage(show_hide); |     app.manage(MenuItems { status, show_hide }); | ||||||
|  |  | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ | |||||||
|     } |     } | ||||||
|   }, |   }, | ||||||
|   "productName": "creddy", |   "productName": "creddy", | ||||||
|   "version": "0.4.9", |   "version": "0.6.1", | ||||||
|   "identifier": "creddy", |   "identifier": "creddy", | ||||||
|   "plugins": {}, |   "plugins": {}, | ||||||
|   "app": { |   "app": { | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ import Unlock from './views/Unlock.svelte'; | |||||||
| // set up app state | // set up app state | ||||||
| invoke('get_config').then(config => $appState.config = config); | invoke('get_config').then(config => $appState.config = config); | ||||||
| invoke('get_session_status').then(status => $appState.sessionStatus = status); | invoke('get_session_status').then(status => $appState.sessionStatus = status); | ||||||
|  | invoke('get_devmode').then(dm => $appState.devmode = dm) | ||||||
| getVersion().then(version => $appState.appVersion = version); | getVersion().then(version => $appState.appVersion = version); | ||||||
| invoke('get_setup_errors') | invoke('get_setup_errors') | ||||||
|     .then(errs => { |     .then(errs => { | ||||||
| @@ -70,3 +71,9 @@ acceptRequest(); | |||||||
|     <!-- normal operation --> |     <!-- normal operation --> | ||||||
|     <svelte:component this="{$currentView}" /> |     <svelte:component this="{$currentView}" /> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
|  | {#if $appState.devmode } | ||||||
|  |     <div class="fixed left-0 bottom-0 right-0 py-1 bg-warning text-xs text-center text-warning-content"> | ||||||
|  |         This is a development build of Creddy. | ||||||
|  |     </div> | ||||||
|  | {/if} | ||||||
|   | |||||||
| @@ -4,10 +4,15 @@ | |||||||
|     export let value = ''; |     export let value = ''; | ||||||
|     export let placeholder = ''; |     export let placeholder = ''; | ||||||
|     export let autofocus = false; |     export let autofocus = false; | ||||||
|  |     export let show = false; | ||||||
|     let classes = ''; |     let classes = ''; | ||||||
|     export {classes as class}; |     export {classes as class}; | ||||||
|  |  | ||||||
|     let show = false; |     let input; | ||||||
|  |  | ||||||
|  |     export function focus() { | ||||||
|  |         input.focus(); | ||||||
|  |     } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -21,6 +26,7 @@ | |||||||
|  |  | ||||||
| <div class="join w-full has-[:focus]:outline outline-2 outline-offset-2 outline-base-content/20"> | <div class="join w-full has-[:focus]:outline outline-2 outline-offset-2 outline-base-content/20"> | ||||||
|     <input |     <input | ||||||
|  |         bind:this={input} | ||||||
|         type={show ? 'text' : 'password'} |         type={show ? 'text' : 'password'} | ||||||
|         {value} {placeholder} {autofocus} |         {value} {placeholder} {autofocus} | ||||||
|         on:input={e => value = e.target.value} |         on:input={e => value = e.target.value} | ||||||
|   | |||||||
| @@ -7,9 +7,10 @@ | |||||||
|     import ShowResponse from './approve/ShowResponse.svelte'; |     import ShowResponse from './approve/ShowResponse.svelte'; | ||||||
|     import Unlock from './Unlock.svelte'; |     import Unlock from './Unlock.svelte'; | ||||||
|  |  | ||||||
|  |     console.log($appState.currentRequest); | ||||||
|  |  | ||||||
|     // Extra 50ms so the window can finish disappearing before the redraw |     // Extra 50ms so the window can finish disappearing before the redraw | ||||||
|     const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 50); |     const rehideDelay = Math.min(5000, $appState.config.rehide_ms + 100); | ||||||
|  |  | ||||||
|     let alert; |     let alert; | ||||||
|     let success = false; |     let success = false; | ||||||
|   | |||||||
| @@ -6,9 +6,8 @@ | |||||||
|  |  | ||||||
|     import AwsCredential from './credentials/AwsCredential.svelte'; |     import AwsCredential from './credentials/AwsCredential.svelte'; | ||||||
|     import ConfirmDelete from './credentials/ConfirmDelete.svelte'; |     import ConfirmDelete from './credentials/ConfirmDelete.svelte'; | ||||||
|  |     import DockerCredential from './credentials/DockerCredential.svelte'; | ||||||
|     import SshKey from './credentials/SshKey.svelte'; |     import SshKey from './credentials/SshKey.svelte'; | ||||||
|     // import NewSshKey from './credentials/NewSshKey.svelte'; |  | ||||||
|     // import EditSshKey from './credentials/EditSshKey.svelte'; |  | ||||||
|     import Icon from '../ui/Icon.svelte'; |     import Icon from '../ui/Icon.svelte'; | ||||||
|     import Nav from '../ui/Nav.svelte'; |     import Nav from '../ui/Nav.svelte'; | ||||||
|  |  | ||||||
| @@ -16,6 +15,7 @@ | |||||||
|     let records = null |     let records = null | ||||||
|     $: awsRecords = (records || []).filter(r => r.credential.type === 'AwsBase'); |     $: awsRecords = (records || []).filter(r => r.credential.type === 'AwsBase'); | ||||||
|     $: sshRecords = (records || []).filter(r => r.credential.type === 'Ssh'); |     $: sshRecords = (records || []).filter(r => r.credential.type === 'Ssh'); | ||||||
|  |     $: dockerRecords = (records || []).filter(r => r.credential.type === 'Docker'); | ||||||
|  |  | ||||||
|     let defaults = writable({}); |     let defaults = writable({}); | ||||||
|     async function loadCreds() { |     async function loadCreds() { | ||||||
| @@ -47,6 +47,17 @@ | |||||||
|         records = records; |         records = records; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     function newDocker() { | ||||||
|  |         records.push({ | ||||||
|  |             id: crypto.randomUUID(), | ||||||
|  |             name: null, | ||||||
|  |             is_default: false, | ||||||
|  |             credential: {type: 'Docker', ServerURL: '', Username: '', Secret: ''}, | ||||||
|  |             isNew: true, | ||||||
|  |         }); | ||||||
|  |         records = records; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     let confirmDelete; |     let confirmDelete; | ||||||
|     function handleDelete(evt) { |     function handleDelete(evt) { | ||||||
|         const record = evt.detail; |         const record = evt.detail; | ||||||
| @@ -117,6 +128,29 @@ | |||||||
|         {/if} |         {/if} | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|  |     <div class="flex flex-col gap-y-4"> | ||||||
|  |         <div class="divider"> | ||||||
|  |             <h2 class="text-xl font-bold">Docker credentials</h2> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         {#if dockerRecords.length > 0} | ||||||
|  |             {#each dockerRecords as record (record.id)} | ||||||
|  |                 <DockerCredential {record} on:save={loadCreds} on:delete={handleDelete} /> | ||||||
|  |             {/each} | ||||||
|  |             <button class="btn btn-primary btn-wide mx-auto" on:click={newDocker}> | ||||||
|  |                 <Icon name="plus-circle-mini" class="size-5" /> | ||||||
|  |                 Add | ||||||
|  |             </button> | ||||||
|  |         {:else if records !== null} | ||||||
|  |             <div class="flex flex-col gap-6 items-center rounded-box border-2 border-dashed border-neutral-content/30 p-6"> | ||||||
|  |                 <div>You have no saved Docker credentials.</div> | ||||||
|  |                 <button class="btn btn-primary btn-wide mx-auto" on:click={newDocker}> | ||||||
|  |                     <Icon name="plus-circle-mini" class="size-5" /> | ||||||
|  |                     Add | ||||||
|  |                 </button> | ||||||
|  |             </div> | ||||||
|  |         {/if} | ||||||
|  |     </div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
| <ConfirmDelete bind:this={confirmDelete} on:confirm={loadCreds} /> | <ConfirmDelete bind:this={confirmDelete} on:confirm={loadCreds} /> | ||||||
|   | |||||||
| @@ -34,9 +34,14 @@ | |||||||
|         } |         } | ||||||
|  |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     let input; | ||||||
|  |     onMount(() => input.focus()); | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | <svelte:window on:focus={input.focus} /> | ||||||
|  |  | ||||||
| <div class="fixed top-0 w-full p-2 text-center"> | <div class="fixed top-0 w-full p-2 text-center"> | ||||||
|     <h1 class="text-3xl font-bold">Creddy is locked</h1> |     <h1 class="text-3xl font-bold">Creddy is locked</h1> | ||||||
| </div> | </div> | ||||||
| @@ -52,7 +57,11 @@ | |||||||
|         <ErrorAlert bind:this="{alert}" /> |         <ErrorAlert bind:this="{alert}" /> | ||||||
|  |  | ||||||
|         <!-- svelte-ignore a11y-autofocus --> |         <!-- svelte-ignore a11y-autofocus --> | ||||||
|         <PassphraseInput autofocus="true" bind:value={passphrase} placeholder="correct horse battery staple" /> |         <PassphraseInput | ||||||
|  |             bind:this={input} | ||||||
|  |             bind:value={passphrase} | ||||||
|  |             placeholder="correct horse battery staple" | ||||||
|  |         /> | ||||||
|     </label> |     </label> | ||||||
|  |  | ||||||
|     <button type="submit" class="btn btn-primary"> |     <button type="submit" class="btn btn-primary"> | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ | |||||||
|     // Extract executable name from full path |     // Extract executable name from full path | ||||||
|     const client = $appState.currentRequest.client; |     const client = $appState.currentRequest.client; | ||||||
|     const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/); |     const m = client.exe?.match(/\/([^/]+?$)|\\([^\\]+?$)/); | ||||||
|     const appName = m[1] || m[2]; |     const appName = m ? m[1] || m[2] : ''; | ||||||
|  |  | ||||||
|     const dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
|  |  | ||||||
| @@ -26,6 +26,12 @@ | |||||||
|         }; |         }; | ||||||
|         dispatch('response'); |         dispatch('response'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const actionDescriptions = { | ||||||
|  |         Access: 'access your', | ||||||
|  |         Delete: 'delete your', | ||||||
|  |         Save: 'create new', | ||||||
|  |     }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -44,9 +50,15 @@ | |||||||
| <div class="space-y-1 mb-4"> | <div class="space-y-1 mb-4"> | ||||||
|     <h2 class="text-xl font-bold"> |     <h2 class="text-xl font-bold"> | ||||||
|         {#if $appState.currentRequest.type === 'Aws'} |         {#if $appState.currentRequest.type === 'Aws'} | ||||||
|             {appName ? `"${appName}"` : 'An appplication'} would like to access your AWS credentials. |             {#if $appState.currentRequest.name} | ||||||
|  |                 {appName ? `"${appName}"` : 'An appplication'} would like to access your AWS access key "{$appState.currentRequest.name}". | ||||||
|  |             {:else} | ||||||
|  |                 {appName ? `"${appName}"` : 'An appplication'} would like to access your default AWS access key | ||||||
|  |             {/if} | ||||||
|         {:else if $appState.currentRequest.type === 'Ssh'} |         {:else if $appState.currentRequest.type === 'Ssh'} | ||||||
|             {appName ? `"${appName}"` : 'An application'} would like to use your SSH key "{$appState.currentRequest.key_name}". |             {appName ? `"${appName}"` : 'An application'} would like to use your SSH key "{$appState.currentRequest.key_name}". | ||||||
|  |         {:else if $appState.currentRequest.type === 'Docker'} | ||||||
|  |             {appName ? `"${appName}"` : 'An application'} would like to {actionDescriptions[$appState.currentRequest.action]} Docker credentials for <code>{$appState.currentRequest.server_url}</code>. | ||||||
|         {/if} |         {/if} | ||||||
|     </h2> |     </h2> | ||||||
|  |  | ||||||
| @@ -55,6 +67,8 @@ | |||||||
|         <code class="">{@html client.exe ? breakPath(client.exe) : 'Unknown'}</code> |         <code class="">{@html client.exe ? breakPath(client.exe) : 'Unknown'}</code> | ||||||
|         <div class="text-right">PID:</div> |         <div class="text-right">PID:</div> | ||||||
|         <code>{client.pid}</code> |         <code>{client.pid}</code> | ||||||
|  |         <div class="text-right">User:</div> | ||||||
|  |         <code>{client.username ?? 'Unknown'}</code> | ||||||
|     </div> |     </div> | ||||||
| </div> | </div> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,13 +5,12 @@ | |||||||
|  |  | ||||||
|     import ErrorAlert from '../../ui/ErrorAlert.svelte'; |     import ErrorAlert from '../../ui/ErrorAlert.svelte'; | ||||||
|     import Icon from '../../ui/Icon.svelte'; |     import Icon from '../../ui/Icon.svelte'; | ||||||
|  |     import PassphraseInput from '../../ui/PassphraseInput.svelte'; | ||||||
|  |  | ||||||
|  |  | ||||||
|     export let record; |     export let record; | ||||||
|     export let defaults; |     export let defaults; | ||||||
|  |  | ||||||
|     import PassphraseInput from '../../ui/PassphraseInput.svelte'; |  | ||||||
|  |  | ||||||
|  |  | ||||||
|     const dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
|  |  | ||||||
|     let showDetails = record.isNew ? true : false; |     let showDetails = record.isNew ? true : false; | ||||||
|   | |||||||
| @@ -26,9 +26,12 @@ | |||||||
|         if (record.credential.type === 'AwsBase') { |         if (record.credential.type === 'AwsBase') { | ||||||
|             return 'AWS credential'; |             return 'AWS credential'; | ||||||
|         } |         } | ||||||
|         if (record.credential.type === 'Ssh') { |         else if (record.credential.type === 'Ssh') { | ||||||
|             return 'SSH key'; |             return 'SSH key'; | ||||||
|         } |         } | ||||||
|  |         else { | ||||||
|  |             return `${record.credential.type} credential`; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										112
									
								
								src/views/credentials/DockerCredential.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/views/credentials/DockerCredential.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | |||||||
|  | <script> | ||||||
|  |  | ||||||
|  |     import { createEventDispatcher } from 'svelte'; | ||||||
|  |     import { fade, slide } from 'svelte/transition'; | ||||||
|  |     import { invoke } from '@tauri-apps/api/core'; | ||||||
|  |  | ||||||
|  |     import ErrorAlert from '../../ui/ErrorAlert.svelte'; | ||||||
|  |     import Icon from '../../ui/Icon.svelte'; | ||||||
|  |     import PassphraseInput from '../../ui/PassphraseInput.svelte'; | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     export let record; | ||||||
|  |  | ||||||
|  |     let local = JSON.parse(JSON.stringify(record)); | ||||||
|  |     $: isModified = JSON.stringify(local) !== JSON.stringify(record); | ||||||
|  |     let showDetails = record?.isNew; | ||||||
|  |  | ||||||
|  |     let alert; | ||||||
|  |     const dispatch = createEventDispatcher(); | ||||||
|  |     async function saveCredential() { | ||||||
|  |         await invoke('save_credential', {record: local}); | ||||||
|  |         dispatch('save', local); | ||||||
|  |         showDetails = false; | ||||||
|  |     } | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <div class="rounded-box space-y-4 bg-base-200"> | ||||||
|  |     <div class="flex items-center px-6 py-4 gap-x-4"> | ||||||
|  |         {#if !record.isNew} | ||||||
|  |             {#if showDetails} | ||||||
|  |                 <input | ||||||
|  |                     type="text" | ||||||
|  |                     class="input input-bordered bg-transparent text-lg font-bold grow" | ||||||
|  |                     bind:value={local.name} | ||||||
|  |                 > | ||||||
|  |             {:else} | ||||||
|  |                 <h3 class="text-lg font-bold break-all"> | ||||||
|  |                     {record.name} | ||||||
|  |                 </h3> | ||||||
|  |             {/if} | ||||||
|  |         {/if} | ||||||
|  |  | ||||||
|  |         <div class="join ml-auto"> | ||||||
|  |             <button | ||||||
|  |                 type="button" | ||||||
|  |                 class="btn btn-outline join-item" | ||||||
|  |                 on:click={() => showDetails = !showDetails} | ||||||
|  |             > | ||||||
|  |                 <Icon name="pencil" class="size-6" /> | ||||||
|  |             </button> | ||||||
|  |             <button | ||||||
|  |                 type="button" | ||||||
|  |                 class="btn btn-outline btn-error join-item" | ||||||
|  |                 on:click={() => dispatch('delete', record)} | ||||||
|  |             > | ||||||
|  |                 <Icon name="trash" class="size-6" /> | ||||||
|  |             </button> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|  |     {#if showDetails} | ||||||
|  |         <form | ||||||
|  |             transition:slide|local={{duration: 200}} | ||||||
|  |             class=" px-6 pb-4 space-y-4" | ||||||
|  |             on:submit|preventDefault={() => alert.run(saveCredential)} | ||||||
|  |         > | ||||||
|  |             <ErrorAlert bind:this={alert} /> | ||||||
|  |  | ||||||
|  |             <div class="grid grid-cols-[auto_1fr] items-center gap-4"> | ||||||
|  |                 {#if record.isNew} | ||||||
|  |                     <span class="justify-self-end">Name</span> | ||||||
|  |                     <input | ||||||
|  |                         type="text" | ||||||
|  |                         class="input input-bordered bg-transparent" | ||||||
|  |                         bind:value={local.name} | ||||||
|  |                     > | ||||||
|  |                 {/if} | ||||||
|  |  | ||||||
|  |                 <span class="justify-self-end">Server URL</span> | ||||||
|  |                 <input | ||||||
|  |                     type="text" | ||||||
|  |                     class="input input-bordered font-mono bg-transparent" | ||||||
|  |                     bind:value={local.credential.ServerURL} | ||||||
|  |                 > | ||||||
|  |  | ||||||
|  |                 <span class="justify-self-end">Username</span> | ||||||
|  |                 <input | ||||||
|  |                     type="text" | ||||||
|  |                     class="input input-bordered font-mono bg-transparent" | ||||||
|  |                     bind:value={local.credential.Username} | ||||||
|  |                 > | ||||||
|  |  | ||||||
|  |                 <span>Password</span> | ||||||
|  |                 <div class="font-mono"> | ||||||
|  |                     <PassphraseInput class="bg-transparent" bind:value={local.credential.Secret} /> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |  | ||||||
|  |             <div class="flex justify-end"> | ||||||
|  |                 {#if isModified} | ||||||
|  |                     <button | ||||||
|  |                         transition:fade={{duration: 100}} | ||||||
|  |                         type="submit" | ||||||
|  |                         class="btn btn-primary" | ||||||
|  |                     > | ||||||
|  |                         Save | ||||||
|  |                     </button> | ||||||
|  |                 {/if} | ||||||
|  |             </div> | ||||||
|  |         </form> | ||||||
|  |     {/if} | ||||||
|  | </div> | ||||||
| @@ -15,7 +15,6 @@ | |||||||
|     async function saveCredential() { |     async function saveCredential() { | ||||||
|         await invoke('save_credential', {record: local}); |         await invoke('save_credential', {record: local}); | ||||||
|         dispatch('save', local); |         dispatch('save', local); | ||||||
|         showDetails = false; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function copyText(evt) { |     async function copyText(evt) { | ||||||
|   | |||||||
| @@ -13,20 +13,23 @@ | |||||||
|  |  | ||||||
|     let name; |     let name; | ||||||
|     let file; |     let file; | ||||||
|  |     let privateKey = ''; | ||||||
|     let passphrase = ''; |     let passphrase = ''; | ||||||
|     let showDetails = true; |     let showDetails = true; | ||||||
|  |     let mode = 'file'; | ||||||
|  |  | ||||||
|     const dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
|  |  | ||||||
|     let defaultPath = null; |     let defaultPath = null; | ||||||
|     homeDir().then(d => defaultPath = `${d}/.ssh`); |     homeDir().then(d => defaultPath = `${d}/.ssh`); | ||||||
|  |  | ||||||
|  |  | ||||||
|     let alert; |     let alert; | ||||||
|     let saving = false; |     let saving = false; | ||||||
|     async function saveCredential() { |     async function saveCredential() { | ||||||
|         saving = true; |         saving = true; | ||||||
|         try { |         try { | ||||||
|             let key = await invoke('sshkey_from_file', {path: file.path, passphrase}); |             let key = await getKey(); | ||||||
|             const payload = { |             const payload = { | ||||||
|                 id: record.id, |                 id: record.id, | ||||||
|                 name, |                 name, | ||||||
| @@ -41,9 +44,40 @@ | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async function getKey() { | ||||||
|  |         if (mode === 'file') { | ||||||
|  |             return await invoke('sshkey_from_file', {path: file.path, passphrase}); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             return await invoke('sshkey_from_private_key', {privateKey, passphrase}); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  |  | ||||||
|  | <div role="tablist" class="join max-w-sm mx-auto flex justify-center"> | ||||||
|  |         <button | ||||||
|  |             type="button" | ||||||
|  |             role="tab" | ||||||
|  |             class="join-item flex-1 btn border border-primary hover:border-primary" | ||||||
|  |             class:btn-primary={mode === 'file'} | ||||||
|  |             on:click={() => mode = 'file'} | ||||||
|  |         > | ||||||
|  |             From file | ||||||
|  |         </button> | ||||||
|  |         <button | ||||||
|  |             type="button" | ||||||
|  |             role="tab" | ||||||
|  |             class="join-item flex-1 btn border border-primary hover:border-primary" | ||||||
|  |             class:btn-primary={mode === 'direct'} | ||||||
|  |             on:click={() => mode = 'direct'} | ||||||
|  |         > | ||||||
|  |             From private key | ||||||
|  |         </button> | ||||||
|  | </div> | ||||||
|  |  | ||||||
|  |  | ||||||
| <form class="space-y-4" on:submit|preventDefault={alert.run(saveCredential)}> | <form class="space-y-4" on:submit|preventDefault={alert.run(saveCredential)}> | ||||||
|     <ErrorAlert bind:this={alert} /> |     <ErrorAlert bind:this={alert} /> | ||||||
|  |  | ||||||
| @@ -55,15 +89,20 @@ | |||||||
|             bind:value={name} |             bind:value={name} | ||||||
|         > |         > | ||||||
|  |  | ||||||
|  |         {#if mode === 'file'} | ||||||
|             <span class="justify-self-end">File</span> |             <span class="justify-self-end">File</span> | ||||||
|             <FileInput params={{defaultPath}} bind:value={file} on:update={() => name = file.name} /> |             <FileInput params={{defaultPath}} bind:value={file} on:update={() => name = file.name} /> | ||||||
|  |         {:else} | ||||||
|  |             <span class="justify-self-end">Private key</span> | ||||||
|  |             <textarea bind:value={privateKey} rows="5" class="textarea textarea-bordered bg-transparent font-mono whitespace-pre overflow-x-auto"></textarea> | ||||||
|  |         {/if} | ||||||
|  |  | ||||||
|         <span class="justify-self-end">Passphrase</span> |         <span class="justify-self-end">Passphrase</span> | ||||||
|         <PassphraseInput class="bg-transparent" bind:value={passphrase} /> |         <PassphraseInput class="bg-transparent" bind:value={passphrase} /> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div class="flex justify-end"> |     <div class="flex justify-end"> | ||||||
|         {#if file?.path} |         {#if file?.path || privateKey !== ''} | ||||||
|             <button |             <button | ||||||
|                 transition:fade={{duration: 100}} |                 transition:fade={{duration: 100}} | ||||||
|                 type="submit" |                 type="submit" | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ | |||||||
|             {#if record.isNew} |             {#if record.isNew} | ||||||
|                 <NewSshKey {record} on:save on:save={handleSave} /> |                 <NewSshKey {record} on:save on:save={handleSave} /> | ||||||
|             {:else} |             {:else} | ||||||
|                 <EditSshKey bind:local={local} {isModified} on:save /> |                 <EditSshKey bind:local={local} {isModified} on:save={handleSave} on:save /> | ||||||
|             {/if} |             {/if} | ||||||
|         </div> |         </div> | ||||||
|     {/if} |     {/if} | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ | |||||||
|  |  | ||||||
|     const dispatch = createEventDispatcher(); |     const dispatch = createEventDispatcher(); | ||||||
|  |  | ||||||
|  |     let showPassphrase = false; | ||||||
|     let alert; |     let alert; | ||||||
|     let saving = false; |     let saving = false; | ||||||
|     let passphrase = ''; |     let passphrase = ''; | ||||||
| @@ -52,7 +53,6 @@ | |||||||
|         try { |         try { | ||||||
|             await alert.run(async () => { |             await alert.run(async () => { | ||||||
|                 await invoke('set_passphrase', {passphrase}) |                 await invoke('set_passphrase', {passphrase}) | ||||||
|                 throw('something bad happened'); |  | ||||||
|                 $appState.sessionStatus = 'unlocked'; |                 $appState.sessionStatus = 'unlocked'; | ||||||
|                 dispatch('save'); |                 dispatch('save'); | ||||||
|             }); |             }); | ||||||
| @@ -73,6 +73,7 @@ | |||||||
|         </div> |         </div> | ||||||
|         <PassphraseInput |         <PassphraseInput | ||||||
|             bind:value={passphrase} |             bind:value={passphrase} | ||||||
|  |             bind:show={showPassphrase} | ||||||
|             on:input={onInput} |             on:input={onInput} | ||||||
|             placeholder="correct horse battery staple" |             placeholder="correct horse battery staple" | ||||||
|         /> |         /> | ||||||
| @@ -84,6 +85,7 @@ | |||||||
|         </div> |         </div> | ||||||
|         <PassphraseInput |         <PassphraseInput | ||||||
|             bind:value={confirmPassphrase} |             bind:value={confirmPassphrase} | ||||||
|  |             bind:show={showPassphrase} | ||||||
|             on:input={onInput} on:change={onChange} |             on:input={onInput} on:change={onChange} | ||||||
|             placeholder="correct horse battery staple" |             placeholder="correct horse battery staple" | ||||||
|         /> |         /> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user