diff --git a/Cargo.lock b/Cargo.lock index f1fa0d284..6f54bb938 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1043,6 +1043,35 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d7439c3735f405729d52c3fbbe4de140eaf938a1fe47d227c27f8254d4302a5" +[[package]] +name = "dbus" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b" +dependencies = [ + "libc", + "libdbus-sys", + "winapi", +] + +[[package]] +name = "dbus-secret-service" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42a16374481d92aed73ae45b1f120207d8e71d24fb89f357fadbd8f946fd84b" +dependencies = [ + "aes", + "block-padding", + "cbc", + "dbus", + "futures-util", + "hkdf", + "num", + "once_cell", + "rand", + "sha2", +] + [[package]] name = "deadpool" version = "0.10.0" @@ -2206,6 +2235,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "keyring" +version = "4.0.0-rc.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb06f73ca0ea1cbd3858e54404585e33dccb860cb4fc8a66ad5e75a5736f3f19" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "security-framework", + "windows-sys 0.59.0", +] + [[package]] name = "kurbo" version = "0.8.3" @@ -2236,6 +2278,15 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "libdbus-sys" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06085512b750d640299b79be4bad3d2fa90a9c00b1fd9e1b46364f66f0485c72" +dependencies = [ + "pkg-config", +] + [[package]] name = "libmimalloc-sys" version = "0.1.43" @@ -5072,6 +5123,7 @@ dependencies = [ "futures", "http", "insta", + "keyring", "percent-encoding", "reqwest", "reqwest-middleware", diff --git a/Cargo.toml b/Cargo.toml index 14471724d..be72883c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,6 +124,7 @@ indoc = { version = "2.0.5" } itertools = { version = "0.14.0" } jiff = { version = "0.2.0", features = ["serde"] } junction = { version = "1.2.0" } +keyring = { version = "4.0.0-rc.1", features = ["encrypted"] } mailparse = { version = "0.16.0" } md-5 = { version = "0.10.6" } memchr = { version = "2.7.4" } diff --git a/crates/uv-auth/Cargo.toml b/crates/uv-auth/Cargo.toml index cbe4d4787..1e93a4aec 100644 --- a/crates/uv-auth/Cargo.toml +++ b/crates/uv-auth/Cargo.toml @@ -21,6 +21,7 @@ async-trait = { workspace = true } base64 = { workspace = true } futures = { workspace = true } http = { workspace = true } +keyring = { workspace = true } percent-encoding = { workspace = true } reqwest = { workspace = true } reqwest-middleware = { workspace = true } diff --git a/crates/uv-auth/src/keyring.rs b/crates/uv-auth/src/keyring.rs index 41b92114a..10d855370 100644 --- a/crates/uv-auth/src/keyring.rs +++ b/crates/uv-auth/src/keyring.rs @@ -1,11 +1,21 @@ -use std::{io::Write, process::Stdio}; +use rustc_hash::FxHashSet; +use std::{ + io::Write, + process::Stdio, + sync::{LazyLock, RwLock}, +}; use tokio::process::Command; -use tracing::{instrument, trace, warn}; +use tracing::{debug, instrument, trace, warn}; use uv_redacted::DisplaySafeUrl; use uv_warnings::warn_user_once; use crate::credentials::Credentials; +/// Keyring credentials that have been stored during an invocation of uv. +static STORED_KEYRING_URLS: LazyLock = LazyLock::new(StoredKeyringUrls::new); +/// Service name prefix for storing credentials in a keyring. +static UV_SERVICE_PREFIX: &str = "uv-credentials-"; + /// A backend for retrieving credentials from a keyring. /// /// See pip's implementation for reference @@ -17,13 +27,22 @@ pub struct KeyringProvider { #[derive(Debug)] pub(crate) enum KeyringProviderBackend { - /// Use the `keyring` command to fetch credentials. + /// Use system keyring integration to fetch credentials. + Native, + /// Use the external `keyring` command to fetch credentials. Subprocess, #[cfg(test)] Dummy(Vec<(String, &'static str, &'static str)>), } impl KeyringProvider { + /// Create a new [`KeyringProvider::Native`]. + pub fn native() -> Self { + Self { + backend: KeyringProviderBackend::Native, + } + } + /// Create a new [`KeyringProvider::Subprocess`]. pub fn subprocess() -> Self { Self { @@ -31,6 +50,61 @@ impl KeyringProvider { } } + /// Store credentials for the given [`Url`] to the keyring if the + /// keyring provider backend is `Native`. + #[instrument(skip_all, fields(url = % url.to_string(), username))] + pub fn store_if_native(&self, url: &DisplaySafeUrl, credentials: &Credentials) { + let Some(username) = credentials.username() else { + trace!("Unable to store credentials in keyring for {url} due to missing username"); + return; + }; + let Some(password) = credentials.password() else { + trace!("Unable to store credentials in keyring for {url} due to missing password"); + return; + }; + + match &self.backend { + KeyringProviderBackend::Native => { + // Only store credentials if not already stored during this uv invocation. + if !STORED_KEYRING_URLS.contains(url) { + self.store_native(url.as_str(), username, password); + STORED_KEYRING_URLS.insert(url.clone()); + } + } + KeyringProviderBackend::Subprocess => { + trace!("Storing credentials is not supported for `subprocess` keyring"); + } + #[cfg(test)] + KeyringProviderBackend::Dummy(_) => {} + } + } + + /// Store credentials to the system keyring for the given `service_name`/`username` + /// pair. + #[instrument(skip(self))] + fn store_native(&self, service: &str, username: &str, password: &str) { + let prefixed_service = format!("{UV_SERVICE_PREFIX}{service}"); + let entry = match keyring::Entry::new(&prefixed_service, username) { + Ok(entry) => entry, + Err(err) => { + warn_user_once!( + "Unable to store credentials for {service} in the system keyring: {err}" + ); + return; + } + }; + match entry.set_password(password) { + Ok(()) => { + debug!("Storing credentials for {service} in system keyring"); + } + Err(err) => { + warn_user_once!( + "Unable to store credentials for {service} in the system keyring: {err}" + ); + } + } + } + /// Fetch credentials for the given [`Url`] from the keyring. /// /// Returns [`None`] if no password was found for the username or if any errors @@ -55,6 +129,7 @@ impl KeyringProvider { // trace!("Checking keyring for URL {url}"); let mut credentials = match self.backend { + KeyringProviderBackend::Native => self.fetch_native(url.as_str(), username), KeyringProviderBackend::Subprocess => { self.fetch_subprocess(url.as_str(), username).await } @@ -72,6 +147,7 @@ impl KeyringProvider { }; trace!("Checking keyring for host {host}"); credentials = match self.backend { + KeyringProviderBackend::Native => self.fetch_native(&host, username), KeyringProviderBackend::Subprocess => self.fetch_subprocess(&host, username).await, #[cfg(test)] KeyringProviderBackend::Dummy(ref store) => { @@ -175,6 +251,27 @@ impl KeyringProvider { } } + #[instrument(skip(self))] + fn fetch_native(&self, service: &str, username: Option<&str>) -> Option<(String, String)> { + let prefixed_service = format!("{UV_SERVICE_PREFIX}{service}"); + let username = username?; + if let Ok(entry) = keyring::Entry::new(&prefixed_service, username) { + match entry.get_password() { + Ok(password) => return Some((username.to_string(), password)), + Err(keyring::Error::NoEntry) => { + debug!("No entry found in system keyring for {service}"); + } + Err(err) => { + warn_user_once!( + "Unable to fetch credentials for {service} from system keyring: {}", + err + ); + } + } + } + None + } + #[cfg(test)] fn fetch_dummy( store: &Vec<(String, &'static str, &'static str)>, @@ -213,6 +310,23 @@ impl KeyringProvider { } } +/// Keyring credentials that have been stored during an invocation of uv. +struct StoredKeyringUrls(RwLock>); + +impl StoredKeyringUrls { + pub(crate) fn new() -> Self { + Self(RwLock::new(FxHashSet::default())) + } + + pub(crate) fn contains(&self, url: &DisplaySafeUrl) -> bool { + self.0.read().unwrap().contains(url) + } + + pub(crate) fn insert(&self, url: DisplaySafeUrl) -> bool { + self.0.write().unwrap().insert(url) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 28bbfb07a..8cb4b7e76 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -217,7 +217,14 @@ impl Middleware for AuthMiddleware { if credentials.password().is_some() { trace!("Request for {url} is fully authenticated"); return self - .complete_request(None, request, extensions, next, auth_policy) + .complete_request( + None, + request, + extensions, + next, + maybe_index_url, + auth_policy, + ) .await; } @@ -299,7 +306,14 @@ impl Middleware for AuthMiddleware { trace!("Retrying request for {url} with credentials from cache {credentials:?}"); retry_request = credentials.authenticate(retry_request); return self - .complete_request(None, retry_request, extensions, next, auth_policy) + .complete_request( + None, + retry_request, + extensions, + next, + maybe_index_url, + auth_policy, + ) .await; } } @@ -323,6 +337,7 @@ impl Middleware for AuthMiddleware { retry_request, extensions, next, + maybe_index_url, auth_policy, ) .await; @@ -333,7 +348,14 @@ impl Middleware for AuthMiddleware { trace!("Retrying request for {url} with username from cache {credentials:?}"); retry_request = credentials.authenticate(retry_request); return self - .complete_request(None, retry_request, extensions, next, auth_policy) + .complete_request( + None, + retry_request, + extensions, + next, + maybe_index_url, + auth_policy, + ) .await; } } @@ -358,6 +380,7 @@ impl AuthMiddleware { request: Request, extensions: &mut Extensions, next: Next<'_>, + index_url: Option<&DisplaySafeUrl>, auth_policy: AuthPolicy, ) -> reqwest_middleware::Result { let Some(credentials) = credentials else { @@ -375,6 +398,9 @@ impl AuthMiddleware { .as_ref() .is_ok_and(|response| response.error_for_status_ref().is_ok()) { + if let (Some(index_url), Some(keyring)) = (index_url, &self.keyring) { + keyring.store_if_native(index_url, &credentials); + } trace!("Updating cached credentials for {url} to {credentials:?}"); self.cache().insert(&url, credentials); } @@ -399,7 +425,14 @@ impl AuthMiddleware { if credentials.password().is_some() { trace!("Request for {url} already contains username and password"); return self - .complete_request(Some(credentials), request, extensions, next, auth_policy) + .complete_request( + Some(credentials), + request, + extensions, + next, + index_url, + auth_policy, + ) .await; } @@ -420,7 +453,14 @@ impl AuthMiddleware { // Do not insert already-cached credentials let credentials = None; return self - .complete_request(credentials, request, extensions, next, auth_policy) + .complete_request( + credentials, + request, + extensions, + next, + index_url, + auth_policy, + ) .await; } @@ -458,8 +498,15 @@ impl AuthMiddleware { Some(credentials) }; - self.complete_request(credentials, request, extensions, next, auth_policy) - .await + self.complete_request( + credentials, + request, + extensions, + next, + index_url, + auth_policy, + ) + .await } /// Fetch credentials for a URL. diff --git a/crates/uv-configuration/src/authentication.rs b/crates/uv-configuration/src/authentication.rs index a6773c81f..cc427130a 100644 --- a/crates/uv-configuration/src/authentication.rs +++ b/crates/uv-configuration/src/authentication.rs @@ -9,6 +9,8 @@ pub enum KeyringProviderType { /// Do not use keyring for credential lookup. #[default] Disabled, + /// Use the system keyring for credential lookup. + Native, /// Use the `keyring` command for credential lookup. Subprocess, // /// Not yet implemented @@ -22,6 +24,7 @@ impl KeyringProviderType { pub fn to_provider(&self) -> Option { match self { Self::Disabled => None, + Self::Native => Some(KeyringProvider::native()), Self::Subprocess => Some(KeyringProvider::subprocess()), } } diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 01b5184c8..a852a22d4 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -148,6 +148,7 @@ uv run [OPTIONS] [COMMAND]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -506,6 +507,7 @@ uv add [OPTIONS] >

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -699,6 +701,7 @@ uv remove [OPTIONS] ...

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -880,6 +883,7 @@ uv version [OPTIONS] [VALUE]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -1077,6 +1081,7 @@ uv sync [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -1317,6 +1322,7 @@ uv lock [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used when building source distributions.

@@ -1495,6 +1501,7 @@ uv export [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used when building source distributions.

@@ -1686,6 +1693,7 @@ uv tree [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used when building source distributions.

@@ -1950,6 +1958,7 @@ uv tool run [OPTIONS] [COMMAND]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -2124,6 +2133,7 @@ uv tool install [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -2290,6 +2300,7 @@ uv tool upgrade [OPTIONS] ...

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -3461,6 +3472,7 @@ uv pip compile [OPTIONS] >

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used when building source distributions.

@@ -3752,6 +3764,7 @@ uv pip sync [OPTIONS] ...

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -4021,6 +4034,7 @@ uv pip install [OPTIONS] |--editable May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

Defaults to clone (also known as Copy-on-Write) on macOS, and hardlink on Linux and Windows.

@@ -4263,6 +4277,7 @@ uv pip uninstall [OPTIONS] >

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup
--managed-python

Require use of uv-managed Python versions.

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

@@ -4441,6 +4456,7 @@ uv pip list [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup
--managed-python

Require use of uv-managed Python versions.

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

@@ -4616,6 +4632,7 @@ uv pip tree [OPTIONS]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup
--managed-python

Require use of uv-managed Python versions.

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

@@ -4808,6 +4825,7 @@ uv venv [OPTIONS] [PATH]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used for installing seed packages.

@@ -4963,6 +4981,7 @@ uv build [OPTIONS] [SRC]

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup

The method to use when installing packages from the global cache.

This option is only used when building source distributions.

@@ -5114,6 +5133,7 @@ uv publish --publish-url https://upload.pypi.org/legacy/ --check-url https://pyp

May also be set with the UV_KEYRING_PROVIDER environment variable.

Possible values:

  • disabled: Do not use keyring for credential lookup
  • +
  • native: Use the system keyring for credential lookup
  • subprocess: Use the keyring command for credential lookup
--managed-python

Require use of uv-managed Python versions.

By default, uv prefers using Python versions it manages. However, it will use system Python versions if a uv-managed Python is not installed. This option disables use of system Python versions.

diff --git a/uv.schema.json b/uv.schema.json index d0d4051d3..840686a67 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -1123,6 +1123,11 @@ "type": "string", "const": "disabled" }, + { + "description": "Use the system keyring for credential lookup.", + "type": "string", + "const": "native" + }, { "description": "Use the `keyring` command for credential lookup.", "type": "string",