From f8003b182cd759d5589ca9fc6f33774eef47dd65 Mon Sep 17 00:00:00 2001
From: John Mumm
Date: Thu, 3 Jul 2025 15:10:06 +0200
Subject: [PATCH] Integrate system keyring via `native` keyring provider
backend
---
Cargo.lock | 52 ++++++++
Cargo.toml | 1 +
crates/uv-auth/Cargo.toml | 1 +
crates/uv-auth/src/keyring.rs | 120 +++++++++++++++++-
crates/uv-auth/src/middleware.rs | 61 ++++++++-
crates/uv-configuration/src/authentication.rs | 3 +
docs/reference/cli.md | 20 +++
uv.schema.json | 5 +
8 files changed, 253 insertions(+), 10 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 3c3c9895d..03a6338ca 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1053,6 +1053,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"
@@ -2218,6 +2247,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"
@@ -2248,6 +2290,15 @@ version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
+[[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"
@@ -5108,6 +5159,7 @@ dependencies = [
"futures",
"http",
"insta",
+ "keyring",
"percent-encoding",
"reqwest",
"reqwest-middleware",
diff --git a/Cargo.toml b/Cargo.toml
index 41f8b5d65..ca1b23b1e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -125,6 +125,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..779ab2823 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 b24860531..5e419bb69 100644
--- a/docs/reference/cli.md
+++ b/docs/reference/cli.md
@@ -149,6 +149,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
--link-mode link-modeThe 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.
@@ -508,6 +509,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
--link-mode link-modeThe 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.
@@ -706,6 +708,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
--link-mode link-modeThe 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.
@@ -888,6 +891,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
--link-mode link-modeThe 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.
@@ -1086,6 +1090,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
--link-mode link-modeThe 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.
@@ -1328,6 +1333,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
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used when building source distributions.
@@ -1507,6 +1513,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
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used when building source distributions.
@@ -1699,6 +1706,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
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used when building source distributions.
@@ -2040,6 +2048,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
--link-mode link-modeThe 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.
@@ -2215,6 +2224,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
--link-mode link-modeThe 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.
@@ -2382,6 +2392,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
--link-mode link-modeThe 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.
@@ -3556,6 +3567,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
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used when building source distributions.
@@ -3850,6 +3862,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
--link-mode link-modeThe 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.
@@ -4122,6 +4135,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
--link-mode link-modeThe 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.
@@ -4367,6 +4381,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-pythonRequire 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.
@@ -4545,6 +4560,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-pythonRequire 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.
@@ -4720,6 +4736,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-pythonRequire 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.
@@ -4912,6 +4929,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
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used for installing seed packages.
@@ -5068,6 +5086,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
--link-mode link-modeThe method to use when installing packages from the global cache.
This option is only used when building source distributions.
@@ -5220,6 +5239,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-pythonRequire 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 6deddd4be..710c28167 100644
--- a/uv.schema.json
+++ b/uv.schema.json
@@ -1144,6 +1144,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",