-
Notifications
You must be signed in to change notification settings - Fork 1.5k
[BEEEP|PM27032] Clean ssh agent rewrite #16265
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 30 commits
4c0e7a4
e652b5d
8272221
458c865
9d399fe
7908056
1fb20e8
0f36693
e387125
d524785
c7be485
bc0819b
b49e3c9
6aecbc4
8696497
5818e0f
be0011c
b92377b
1905f3f
3ae5786
5857782
64583e3
fa6e86f
eebdc02
6a7b2a1
cd073ad
dc155cd
c222e7d
0e2412f
51a1d52
e21304c
98b0e87
17ac7a3
c8775c6
d87d772
1d4167f
fd1331d
1eb3a71
96fa507
164c0bd
5fbecdd
4f07320
f1482b8
538bbc7
90bcfa4
5ac9ae5
5b4dd5c
cf3c676
616ffb4
4a9186c
81771da
f91b376
dd2074b
c90b1c2
2e2b251
8887b6a
7812458
069b3e1
141728f
419320e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1053,3 +1053,194 @@ pub mod autotype { | |
| }) | ||
| } | ||
| } | ||
|
|
||
| #[napi] | ||
| pub mod sshagent_v2 { | ||
| use std::sync::Arc; | ||
|
|
||
| use napi::{ | ||
| bindgen_prelude::Promise, | ||
| threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction}, | ||
| }; | ||
| use ssh_agent::agent::{ui_requester, PlatformListener}; | ||
| use ssh_agent::{ | ||
| self, | ||
| agent::{ui_requester::UiRequestMessage, BitwardenDesktopAgent}, | ||
| memory::UnlockedSshItem, | ||
| protocol::types::KeyPair, | ||
| }; | ||
| use tokio::{self, sync::Mutex}; | ||
| use tracing::{error, info}; | ||
|
|
||
| #[napi] | ||
| pub struct SshAgentState { | ||
| agent: BitwardenDesktopAgent, | ||
| } | ||
|
Comment on lines
+1081
to
+1083
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems like there aren't any impls on this struct and it has a single member. Is there a reason not to just use the It'd be helpful to add a doc comment describing what this struct's purpose is, if it's to be kept.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This struct has the |
||
|
|
||
| #[napi(object)] | ||
| pub struct PrivateKey { | ||
| pub private_key: String, | ||
| pub name: String, | ||
| pub cipher_id: String, | ||
| } | ||
|
|
||
| #[napi(object)] | ||
| pub struct SshKey { | ||
| pub private_key: String, | ||
| pub public_key: String, | ||
| pub key_fingerprint: String, | ||
| } | ||
|
|
||
| #[napi(object)] | ||
| pub struct SshUIRequest { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To me, SSH request sounds like the low level information that the agent itself gets from a client. UI request already has the information parsed, and additional metadata, such as the requesting process added. I don't know if there is a better name than this, but it is meant to be shown in a graphical user interface, the only implementation of which is the desktop client.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That said, I don't really like this struct, and would much prefer to expose an enum not having to do this weird conversion. but, the intent of this PR is to keep the interface exposed out of |
||
| pub cipher_id: Option<String>, | ||
| pub is_list: bool, | ||
| pub process_name: String, | ||
| pub is_forwarding: bool, | ||
| pub namespace: Option<String>, | ||
| } | ||
|
|
||
| #[allow(clippy::unused_async)] // FIXME: Remove unused async! | ||
quexten marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| #[napi] | ||
| pub async fn serve( | ||
| callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>, | ||
| ) -> napi::Result<SshAgentState> { | ||
| let (auth_request_tx, mut auth_request_rx) = | ||
| tokio::sync::mpsc::channel::<ssh_agent::agent::ui_requester::UiRequestMessage>(32); | ||
| let (auth_response_tx, auth_response_rx) = | ||
| tokio::sync::broadcast::channel::<(u32, bool)>(32); | ||
quexten marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx)); | ||
| let ui_requester = | ||
| ui_requester::UiRequester::new(auth_request_tx, Arc::new(Mutex::new(auth_response_rx))); | ||
|
|
||
| tokio::spawn(async move { | ||
| let _ = ui_requester; | ||
quexten marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| while let Some(request) = auth_request_rx.recv().await { | ||
| let cloned_response_tx_arc = auth_response_tx_arc.clone(); | ||
| let cloned_callback = callback.clone(); | ||
| tokio::spawn(async move { | ||
quexten marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| let auth_response_tx_arc = cloned_response_tx_arc; | ||
| let callback = cloned_callback; | ||
|
|
||
| let js_request = match request.clone() { | ||
| UiRequestMessage::ListRequest { | ||
| request_id: _, | ||
| connection_info, | ||
| } => SshUIRequest { | ||
| cipher_id: None, | ||
| is_list: true, | ||
| process_name: connection_info.peer_info().process_name().to_string(), | ||
| is_forwarding: connection_info.is_forwarding(), | ||
| namespace: None, | ||
| }, | ||
| UiRequestMessage::AuthRequest { | ||
| request_id: _, | ||
| connection_info, | ||
| cipher_id, | ||
| } => SshUIRequest { | ||
| cipher_id: Some(cipher_id), | ||
| is_list: false, | ||
| process_name: connection_info.peer_info().process_name().to_string(), | ||
| is_forwarding: connection_info.is_forwarding(), | ||
| namespace: None, | ||
| }, | ||
| UiRequestMessage::SignRequest { | ||
| request_id: _, | ||
| connection_info, | ||
| cipher_id, | ||
| namespace, | ||
| } => SshUIRequest { | ||
| cipher_id: Some(cipher_id), | ||
| is_list: false, | ||
| process_name: connection_info.peer_info().process_name().to_string(), | ||
| is_forwarding: connection_info.is_forwarding(), | ||
| namespace: Some(namespace), | ||
quexten marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }, | ||
| }; | ||
|
|
||
| let promise_result: Result<Promise<bool>, napi::Error> = | ||
| callback.call_async(Ok(js_request)).await; | ||
| match promise_result { | ||
| Ok(promise_result) => match promise_result.await { | ||
| Ok(result) => { | ||
| let _ = auth_response_tx_arc | ||
| .lock() | ||
| .await | ||
| .send((request.id(), result)) | ||
| .expect("should be able to send auth response to agent"); | ||
| } | ||
| Err(e) => { | ||
| error!(error = %e, "Calling UI callback promise was rejected"); | ||
| let _ = auth_response_tx_arc | ||
| .lock() | ||
| .await | ||
| .send((request.id(), false)) | ||
| .expect("should be able to send auth response to agent"); | ||
| } | ||
| }, | ||
| Err(e) => { | ||
| error!(error = %e, "Calling UI callback could not create promise"); | ||
| let _ = auth_response_tx_arc | ||
| .lock() | ||
| .await | ||
| .send((request.id(), false)) | ||
| .expect("should be able to send auth response to agent"); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| let agent = BitwardenDesktopAgent::new(ui_requester); | ||
| let agent_copy = agent.clone(); | ||
| PlatformListener::spawn_listeners(agent_copy); | ||
|
|
||
| Ok(SshAgentState { agent }) | ||
| } | ||
|
|
||
| #[napi] | ||
| pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> { | ||
| info!("Stopping SSH Agent"); | ||
| agent_state.agent.stop(); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[napi] | ||
| pub fn is_running(agent_state: &mut SshAgentState) -> bool { | ||
| agent_state.agent.is_running() | ||
| } | ||
|
|
||
| #[napi] | ||
| pub fn set_keys( | ||
| agent_state: &mut SshAgentState, | ||
| new_keys: Vec<PrivateKey>, | ||
| ) -> napi::Result<()> { | ||
| agent_state.agent.set_keys( | ||
| new_keys | ||
| .iter() | ||
| .filter_map(|k| { | ||
| let private_key = | ||
| ssh_agent::protocol::types::PrivateKey::try_from(k.private_key.clone()) | ||
| .ok()?; | ||
| Some(UnlockedSshItem::new( | ||
| KeyPair::new(private_key, k.name.clone()), | ||
| k.cipher_id.clone(), | ||
| )) | ||
| }) | ||
| .collect(), | ||
| ); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[napi] | ||
| pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> { | ||
| agent_state.agent.lock(); | ||
| Ok(()) | ||
| } | ||
|
|
||
| #[napi] | ||
| pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> { | ||
| set_keys(agent_state, vec![]) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| [package] | ||
| name = "ssh_agent" | ||
| edition = { workspace = true } | ||
| license = { workspace = true } | ||
| version = { workspace = true } | ||
| publish = { workspace = true } | ||
|
|
||
| [features] | ||
| default = ["dep:windows"] | ||
| manual_test = [] | ||
|
|
||
| [target.'cfg(windows)'.dependencies] | ||
| windows = { workspace = true, features = [ | ||
| "Win32_System_Pipes", | ||
| ], optional = true } | ||
| windows-future = { workspace = true } | ||
|
|
||
| [dependencies] | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note for reviewers: We cannot use workspace deps. These conflict with
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense ,though it looks like there are workspace deps being used? Did you mean that we can only use some workspace deps? Also noting that we should add a link to Jira ticket for FF removal to make sure we circle back and take care of this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Usually, we prefer to have all deps in the workspace root, and never in specific sub crates. In this case, the specific set of dependencies that must be different in the v2 version of SSH agent, is set in the ssh agent crate. This means that the v1 agent still uses the outdated old deps, while the v2 agent uses the new deps. Both deps are compiled into the binary, but without conflicting. It is fine for the ssh agent crate to use workspace deps as long as they are not conflicting. The ones overwritten / pinned here were conflicting. I will make the FF when all other things look good, let's keep this comment open and unresolved. |
||
| anyhow = { workspace = true } | ||
| base64 = { workspace = true } | ||
| block-padding = { version = "=0.4.0-rc.4" } | ||
| byteorder = { workspace = true } | ||
| bytes = { workspace = true } | ||
| ed25519 = { workspace = true, features = ["pkcs8"] } | ||
| ed25519-dalek = { version = "2.2.0", features = ["rand_core"] } | ||
| futures = { workspace = true } | ||
| homedir = { workspace = true } | ||
| inout = { version = "=0.2.0-rc.6" } | ||
| log = { workspace = true } | ||
| num_enum = "0.7.4" | ||
| p256 = "0.13.2" | ||
| p384 = "0.13.1" | ||
| p521 = "0.13.3" | ||
| pin-project = { workspace = true } | ||
| rsa = { version = "=0.10.0-rc.8", features = ["sha2"] } | ||
| sha2 = "0.10.9" | ||
| signature = "3.0.0-rc.4" | ||
| ssh-encoding = "=0.3.0-rc.2" | ||
| ssh-key = { version = "=0.7.0-rc.3", features = [ | ||
| "encryption", | ||
| "ed25519", | ||
| "rsa", | ||
| "ecdsa", | ||
| "getrandom", | ||
| ] } | ||
| sysinfo = { workspace = true, features = ["windows"] } | ||
| tokio = { workspace = true, features = ["io-util", "sync", "macros", "net", "full"] } | ||
| tokio-util = { workspace = true, features = ["codec"] } | ||
| tracing = { workspace = true } | ||
| tracing-subscriber.workspace = true | ||
|
|
||
| [lints] | ||
| workspace = true | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is great, it was on my mental list to bring this out of core.