summaryrefslogtreecommitdiffstats
path: root/src/tools/cargo/credential
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-18 02:49:50 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-18 02:49:50 +0000
commit9835e2ae736235810b4ea1c162ca5e65c547e770 (patch)
tree3fcebf40ed70e581d776a8a4c65923e8ec20e026 /src/tools/cargo/credential
parentReleasing progress-linux version 1.70.0+dfsg2-1~progress7.99u1. (diff)
downloadrustc-9835e2ae736235810b4ea1c162ca5e65c547e770.tar.xz
rustc-9835e2ae736235810b4ea1c162ca5e65c547e770.zip
Merging upstream version 1.71.1+dfsg1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/tools/cargo/credential')
-rw-r--r--src/tools/cargo/credential/README.md8
-rw-r--r--src/tools/cargo/credential/cargo-credential-1password/Cargo.toml12
-rw-r--r--src/tools/cargo/credential/cargo-credential-1password/src/main.rs314
-rw-r--r--src/tools/cargo/credential/cargo-credential-gnome-secret/Cargo.toml13
-rw-r--r--src/tools/cargo/credential/cargo-credential-gnome-secret/build.rs3
-rw-r--r--src/tools/cargo/credential/cargo-credential-gnome-secret/src/main.rs194
-rw-r--r--src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml13
-rw-r--r--src/tools/cargo/credential/cargo-credential-macos-keychain/src/main.rs58
-rw-r--r--src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml14
-rw-r--r--src/tools/cargo/credential/cargo-credential-wincred/src/main.rs122
-rw-r--r--src/tools/cargo/credential/cargo-credential/Cargo.toml9
-rw-r--r--src/tools/cargo/credential/cargo-credential/README.md41
-rw-r--r--src/tools/cargo/credential/cargo-credential/src/lib.rs106
13 files changed, 907 insertions, 0 deletions
diff --git a/src/tools/cargo/credential/README.md b/src/tools/cargo/credential/README.md
new file mode 100644
index 000000000..168cc71c3
--- /dev/null
+++ b/src/tools/cargo/credential/README.md
@@ -0,0 +1,8 @@
+# Cargo Credential Packages
+
+This directory contains Cargo packages for handling storage of tokens in a
+secure manner.
+
+`cargo-credential` is a generic library to assist writing a credential
+process. The other directories contain implementations that integrate with
+specific credential systems.
diff --git a/src/tools/cargo/credential/cargo-credential-1password/Cargo.toml b/src/tools/cargo/credential/cargo-credential-1password/Cargo.toml
new file mode 100644
index 000000000..8db40e577
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-1password/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "cargo-credential-1password"
+version = "0.2.0"
+edition = "2021"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/rust-lang/cargo"
+description = "A Cargo credential process that stores tokens in a 1password vault."
+
+[dependencies]
+cargo-credential.workspace = true
+serde = { workspace = true, features = ["derive"] }
+serde_json.workspace = true
diff --git a/src/tools/cargo/credential/cargo-credential-1password/src/main.rs b/src/tools/cargo/credential/cargo-credential-1password/src/main.rs
new file mode 100644
index 000000000..4f512b717
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-1password/src/main.rs
@@ -0,0 +1,314 @@
+//! Cargo registry 1password credential process.
+
+use cargo_credential::{Credential, Error};
+use serde::Deserialize;
+use std::io::Read;
+use std::process::{Command, Stdio};
+
+const CARGO_TAG: &str = "cargo-registry";
+
+/// Implementation of 1password keychain access for Cargo registries.
+struct OnePasswordKeychain {
+ account: Option<String>,
+ vault: Option<String>,
+}
+
+/// 1password Login item type, used for the JSON output of `op item get`.
+#[derive(Deserialize)]
+struct Login {
+ fields: Vec<Field>,
+}
+
+#[derive(Deserialize)]
+struct Field {
+ id: String,
+ value: Option<String>,
+}
+
+/// 1password item from `op items list`.
+#[derive(Deserialize)]
+struct ListItem {
+ id: String,
+ urls: Vec<Url>,
+}
+
+#[derive(Deserialize)]
+struct Url {
+ href: String,
+}
+
+impl OnePasswordKeychain {
+ fn new() -> Result<OnePasswordKeychain, Error> {
+ let mut args = std::env::args().skip(1);
+ let mut action = false;
+ let mut account = None;
+ let mut vault = None;
+ while let Some(arg) = args.next() {
+ match arg.as_str() {
+ "--account" => {
+ account = Some(args.next().ok_or("--account needs an arg")?);
+ }
+ "--vault" => {
+ vault = Some(args.next().ok_or("--vault needs an arg")?);
+ }
+ s if s.starts_with('-') => {
+ return Err(format!("unknown option {}", s).into());
+ }
+ _ => {
+ if action {
+ return Err("too many arguments".into());
+ } else {
+ action = true;
+ }
+ }
+ }
+ }
+ Ok(OnePasswordKeychain { account, vault })
+ }
+
+ fn signin(&self) -> Result<Option<String>, Error> {
+ // If there are any session env vars, we'll assume that this is the
+ // correct account, and that the user knows what they are doing.
+ if std::env::vars().any(|(name, _)| name.starts_with("OP_SESSION_")) {
+ return Ok(None);
+ }
+ let mut cmd = Command::new("op");
+ cmd.args(&["signin", "--raw"]);
+ cmd.stdout(Stdio::piped());
+ self.with_tty(&mut cmd)?;
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| format!("failed to spawn `op`: {}", e))?;
+ let mut buffer = String::new();
+ child
+ .stdout
+ .as_mut()
+ .unwrap()
+ .read_to_string(&mut buffer)
+ .map_err(|e| format!("failed to get session from `op`: {}", e))?;
+ if let Some(end) = buffer.find('\n') {
+ buffer.truncate(end);
+ }
+ let status = child
+ .wait()
+ .map_err(|e| format!("failed to wait for `op`: {}", e))?;
+ if !status.success() {
+ return Err(format!("failed to run `op signin`: {}", status).into());
+ }
+ if buffer.is_empty() {
+ // When using CLI integration, `op signin` returns no output,
+ // so there is no need to set the session.
+ return Ok(None);
+ }
+ Ok(Some(buffer))
+ }
+
+ fn make_cmd(&self, session: &Option<String>, args: &[&str]) -> Command {
+ let mut cmd = Command::new("op");
+ cmd.args(args);
+ if let Some(account) = &self.account {
+ cmd.arg("--account");
+ cmd.arg(account);
+ }
+ if let Some(vault) = &self.vault {
+ cmd.arg("--vault");
+ cmd.arg(vault);
+ }
+ if let Some(session) = session {
+ cmd.arg("--session");
+ cmd.arg(session);
+ }
+ cmd
+ }
+
+ fn with_tty(&self, cmd: &mut Command) -> Result<(), Error> {
+ #[cfg(unix)]
+ const IN_DEVICE: &str = "/dev/tty";
+ #[cfg(windows)]
+ const IN_DEVICE: &str = "CONIN$";
+ let stdin = std::fs::OpenOptions::new()
+ .read(true)
+ .write(true)
+ .open(IN_DEVICE)?;
+ cmd.stdin(stdin);
+ Ok(())
+ }
+
+ fn run_cmd(&self, mut cmd: Command) -> Result<String, Error> {
+ cmd.stdout(Stdio::piped());
+ let mut child = cmd
+ .spawn()
+ .map_err(|e| format!("failed to spawn `op`: {}", e))?;
+ let mut buffer = String::new();
+ child
+ .stdout
+ .as_mut()
+ .unwrap()
+ .read_to_string(&mut buffer)
+ .map_err(|e| format!("failed to read `op` output: {}", e))?;
+ let status = child
+ .wait()
+ .map_err(|e| format!("failed to wait for `op`: {}", e))?;
+ if !status.success() {
+ return Err(format!("`op` command exit error: {}", status).into());
+ }
+ Ok(buffer)
+ }
+
+ fn search(&self, session: &Option<String>, index_url: &str) -> Result<Option<String>, Error> {
+ let cmd = self.make_cmd(
+ session,
+ &[
+ "items",
+ "list",
+ "--categories",
+ "Login",
+ "--tags",
+ CARGO_TAG,
+ "--format",
+ "json",
+ ],
+ );
+ let buffer = self.run_cmd(cmd)?;
+ let items: Vec<ListItem> = serde_json::from_str(&buffer)
+ .map_err(|e| format!("failed to deserialize JSON from 1password list: {}", e))?;
+ let mut matches = items
+ .into_iter()
+ .filter(|item| item.urls.iter().any(|url| url.href == index_url));
+ match matches.next() {
+ Some(login) => {
+ // Should this maybe just sort on `updatedAt` and return the newest one?
+ if matches.next().is_some() {
+ return Err(format!(
+ "too many 1password logins match registry `{}`, \
+ consider deleting the excess entries",
+ index_url
+ )
+ .into());
+ }
+ Ok(Some(login.id))
+ }
+ None => Ok(None),
+ }
+ }
+
+ fn modify(
+ &self,
+ session: &Option<String>,
+ id: &str,
+ token: &str,
+ _name: Option<&str>,
+ ) -> Result<(), Error> {
+ let cmd = self.make_cmd(
+ session,
+ &["item", "edit", id, &format!("password={}", token)],
+ );
+ self.run_cmd(cmd)?;
+ Ok(())
+ }
+
+ fn create(
+ &self,
+ session: &Option<String>,
+ index_url: &str,
+ token: &str,
+ name: Option<&str>,
+ ) -> Result<(), Error> {
+ let title = match name {
+ Some(name) => format!("Cargo registry token for {}", name),
+ None => "Cargo registry token".to_string(),
+ };
+ let mut cmd = self.make_cmd(
+ session,
+ &[
+ "item",
+ "create",
+ "--category",
+ "Login",
+ &format!("password={}", token),
+ &format!("url={}", index_url),
+ "--title",
+ &title,
+ "--tags",
+ CARGO_TAG,
+ ],
+ );
+ // For unknown reasons, `op item create` seems to not be happy if
+ // stdin is not a tty. Otherwise it returns with a 0 exit code without
+ // doing anything.
+ self.with_tty(&mut cmd)?;
+ self.run_cmd(cmd)?;
+ Ok(())
+ }
+
+ fn get_token(&self, session: &Option<String>, id: &str) -> Result<String, Error> {
+ let cmd = self.make_cmd(session, &["item", "get", "--format=json", id]);
+ let buffer = self.run_cmd(cmd)?;
+ let item: Login = serde_json::from_str(&buffer)
+ .map_err(|e| format!("failed to deserialize JSON from 1password get: {}", e))?;
+ let password = item.fields.into_iter().find(|item| item.id == "password");
+ match password {
+ Some(password) => password
+ .value
+ .ok_or_else(|| format!("missing password value for entry").into()),
+ None => Err("could not find password field".into()),
+ }
+ }
+
+ fn delete(&self, session: &Option<String>, id: &str) -> Result<(), Error> {
+ let cmd = self.make_cmd(session, &["item", "delete", id]);
+ self.run_cmd(cmd)?;
+ Ok(())
+ }
+}
+
+impl Credential for OnePasswordKeychain {
+ fn name(&self) -> &'static str {
+ env!("CARGO_PKG_NAME")
+ }
+
+ fn get(&self, index_url: &str) -> Result<String, Error> {
+ let session = self.signin()?;
+ if let Some(id) = self.search(&session, index_url)? {
+ self.get_token(&session, &id)
+ } else {
+ return Err(format!(
+ "no 1password entry found for registry `{}`, try `cargo login` to add a token",
+ index_url
+ )
+ .into());
+ }
+ }
+
+ fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
+ let session = self.signin()?;
+ // Check if an item already exists.
+ if let Some(id) = self.search(&session, index_url)? {
+ self.modify(&session, &id, token, name)
+ } else {
+ self.create(&session, index_url, token, name)
+ }
+ }
+
+ fn erase(&self, index_url: &str) -> Result<(), Error> {
+ let session = self.signin()?;
+ // Check if an item already exists.
+ if let Some(id) = self.search(&session, index_url)? {
+ self.delete(&session, &id)?;
+ } else {
+ eprintln!("not currently logged in to `{}`", index_url);
+ }
+ Ok(())
+ }
+}
+
+fn main() {
+ let op = match OnePasswordKeychain::new() {
+ Ok(op) => op,
+ Err(e) => {
+ eprintln!("error: {}", e);
+ std::process::exit(1);
+ }
+ };
+ cargo_credential::main(op);
+}
diff --git a/src/tools/cargo/credential/cargo-credential-gnome-secret/Cargo.toml b/src/tools/cargo/credential/cargo-credential-gnome-secret/Cargo.toml
new file mode 100644
index 000000000..63b3e95cc
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-gnome-secret/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "cargo-credential-gnome-secret"
+version = "0.2.0"
+edition = "2021"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/rust-lang/cargo"
+description = "A Cargo credential process that stores tokens with GNOME libsecret."
+
+[dependencies]
+cargo-credential.workspace = true
+
+[build-dependencies]
+pkg-config.workspace = true
diff --git a/src/tools/cargo/credential/cargo-credential-gnome-secret/build.rs b/src/tools/cargo/credential/cargo-credential-gnome-secret/build.rs
new file mode 100644
index 000000000..9283535af
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-gnome-secret/build.rs
@@ -0,0 +1,3 @@
+fn main() {
+ pkg_config::probe_library("libsecret-1").unwrap();
+}
diff --git a/src/tools/cargo/credential/cargo-credential-gnome-secret/src/main.rs b/src/tools/cargo/credential/cargo-credential-gnome-secret/src/main.rs
new file mode 100644
index 000000000..40972b05d
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-gnome-secret/src/main.rs
@@ -0,0 +1,194 @@
+//! Cargo registry gnome libsecret credential process.
+
+use cargo_credential::{Credential, Error};
+use std::ffi::{CStr, CString};
+use std::os::raw::{c_char, c_int};
+use std::ptr::{null, null_mut};
+
+#[allow(non_camel_case_types)]
+type gchar = c_char;
+
+#[allow(non_camel_case_types)]
+type gboolean = c_int;
+
+type GQuark = u32;
+
+#[repr(C)]
+struct GError {
+ domain: GQuark,
+ code: c_int,
+ message: *mut gchar,
+}
+
+#[repr(C)]
+struct GCancellable {
+ _private: [u8; 0],
+}
+
+#[repr(C)]
+struct SecretSchema {
+ name: *const gchar,
+ flags: SecretSchemaFlags,
+ attributes: [SecretSchemaAttribute; 32],
+}
+
+#[repr(C)]
+#[derive(Copy, Clone)]
+struct SecretSchemaAttribute {
+ name: *const gchar,
+ attr_type: SecretSchemaAttributeType,
+}
+
+#[repr(C)]
+enum SecretSchemaFlags {
+ None = 0,
+}
+
+#[repr(C)]
+#[derive(Copy, Clone)]
+enum SecretSchemaAttributeType {
+ String = 0,
+}
+
+extern "C" {
+ fn secret_password_store_sync(
+ schema: *const SecretSchema,
+ collection: *const gchar,
+ label: *const gchar,
+ password: *const gchar,
+ cancellable: *mut GCancellable,
+ error: *mut *mut GError,
+ ...
+ ) -> gboolean;
+ fn secret_password_clear_sync(
+ schema: *const SecretSchema,
+ cancellable: *mut GCancellable,
+ error: *mut *mut GError,
+ ...
+ ) -> gboolean;
+ fn secret_password_lookup_sync(
+ schema: *const SecretSchema,
+ cancellable: *mut GCancellable,
+ error: *mut *mut GError,
+ ...
+ ) -> *mut gchar;
+}
+
+struct GnomeSecret;
+
+fn label(index_url: &str) -> CString {
+ CString::new(format!("cargo-registry:{}", index_url)).unwrap()
+}
+
+fn schema() -> SecretSchema {
+ let mut attributes = [SecretSchemaAttribute {
+ name: null(),
+ attr_type: SecretSchemaAttributeType::String,
+ }; 32];
+ attributes[0] = SecretSchemaAttribute {
+ name: b"url\0".as_ptr() as *const gchar,
+ attr_type: SecretSchemaAttributeType::String,
+ };
+ SecretSchema {
+ name: b"org.rust-lang.cargo.registry\0".as_ptr() as *const gchar,
+ flags: SecretSchemaFlags::None,
+ attributes,
+ }
+}
+
+impl Credential for GnomeSecret {
+ fn name(&self) -> &'static str {
+ env!("CARGO_PKG_NAME")
+ }
+
+ fn get(&self, index_url: &str) -> Result<String, Error> {
+ let mut error: *mut GError = null_mut();
+ let attr_url = CString::new("url").unwrap();
+ let index_url_c = CString::new(index_url).unwrap();
+ let schema = schema();
+ unsafe {
+ let token_c = secret_password_lookup_sync(
+ &schema,
+ null_mut(),
+ &mut error,
+ attr_url.as_ptr(),
+ index_url_c.as_ptr(),
+ null() as *const gchar,
+ );
+ if !error.is_null() {
+ return Err(format!(
+ "failed to get token: {}",
+ CStr::from_ptr((*error).message).to_str()?
+ )
+ .into());
+ }
+ if token_c.is_null() {
+ return Err(format!("cannot find token for {}", index_url).into());
+ }
+ let token = CStr::from_ptr(token_c)
+ .to_str()
+ .map_err(|e| format!("expected utf8 token: {}", e))?
+ .to_string();
+ Ok(token)
+ }
+ }
+
+ fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
+ let label = label(name.unwrap_or(index_url));
+ let token = CString::new(token).unwrap();
+ let mut error: *mut GError = null_mut();
+ let attr_url = CString::new("url").unwrap();
+ let index_url_c = CString::new(index_url).unwrap();
+ let schema = schema();
+ unsafe {
+ secret_password_store_sync(
+ &schema,
+ b"default\0".as_ptr() as *const gchar,
+ label.as_ptr(),
+ token.as_ptr(),
+ null_mut(),
+ &mut error,
+ attr_url.as_ptr(),
+ index_url_c.as_ptr(),
+ null() as *const gchar,
+ );
+ if !error.is_null() {
+ return Err(format!(
+ "failed to store token: {}",
+ CStr::from_ptr((*error).message).to_str()?
+ )
+ .into());
+ }
+ }
+ Ok(())
+ }
+
+ fn erase(&self, index_url: &str) -> Result<(), Error> {
+ let schema = schema();
+ let mut error: *mut GError = null_mut();
+ let attr_url = CString::new("url").unwrap();
+ let index_url_c = CString::new(index_url).unwrap();
+ unsafe {
+ secret_password_clear_sync(
+ &schema,
+ null_mut(),
+ &mut error,
+ attr_url.as_ptr(),
+ index_url_c.as_ptr(),
+ null() as *const gchar,
+ );
+ if !error.is_null() {
+ return Err(format!(
+ "failed to erase token: {}",
+ CStr::from_ptr((*error).message).to_str()?
+ )
+ .into());
+ }
+ }
+ Ok(())
+ }
+}
+
+fn main() {
+ cargo_credential::main(GnomeSecret);
+}
diff --git a/src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml b/src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml
new file mode 100644
index 000000000..6311b71de
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-macos-keychain/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "cargo-credential-macos-keychain"
+version = "0.2.0"
+edition = "2021"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/rust-lang/cargo"
+description = "A Cargo credential process that stores tokens in a macOS keychain."
+
+[dependencies]
+cargo-credential.workspace = true
+
+[target.'cfg(target_os = "macos")'.dependencies]
+security-framework.workspace = true
diff --git a/src/tools/cargo/credential/cargo-credential-macos-keychain/src/main.rs b/src/tools/cargo/credential/cargo-credential-macos-keychain/src/main.rs
new file mode 100644
index 000000000..4d6ea96d0
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-macos-keychain/src/main.rs
@@ -0,0 +1,58 @@
+//! Cargo registry macos keychain credential process.
+
+#[cfg(target_os = "macos")]
+mod macos {
+ use cargo_credential::{Credential, Error};
+ use security_framework::os::macos::keychain::SecKeychain;
+
+ pub(crate) struct MacKeychain;
+
+ /// The account name is not used.
+ const ACCOUNT: &'static str = "";
+
+ fn registry(registry_name: &str) -> String {
+ format!("cargo-registry:{}", registry_name)
+ }
+
+ impl Credential for MacKeychain {
+ fn name(&self) -> &'static str {
+ env!("CARGO_PKG_NAME")
+ }
+
+ fn get(&self, index_url: &str) -> Result<String, Error> {
+ let keychain = SecKeychain::default().unwrap();
+ let service_name = registry(index_url);
+ let (pass, _item) = keychain.find_generic_password(&service_name, ACCOUNT)?;
+ String::from_utf8(pass.as_ref().to_vec())
+ .map_err(|_| "failed to convert token to UTF8".into())
+ }
+
+ fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
+ let keychain = SecKeychain::default().unwrap();
+ let service_name = registry(name.unwrap_or(index_url));
+ if let Ok((_pass, mut item)) = keychain.find_generic_password(&service_name, ACCOUNT) {
+ item.set_password(token.as_bytes())?;
+ } else {
+ keychain.add_generic_password(&service_name, ACCOUNT, token.as_bytes())?;
+ }
+ Ok(())
+ }
+
+ fn erase(&self, index_url: &str) -> Result<(), Error> {
+ let keychain = SecKeychain::default().unwrap();
+ let service_name = registry(index_url);
+ let (_pass, item) = keychain.find_generic_password(&service_name, ACCOUNT)?;
+ item.delete();
+ Ok(())
+ }
+ }
+}
+
+#[cfg(not(target_os = "macos"))]
+use cargo_credential::UnsupportedCredential as MacKeychain;
+#[cfg(target_os = "macos")]
+use macos::MacKeychain;
+
+fn main() {
+ cargo_credential::main(MacKeychain);
+}
diff --git a/src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml b/src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml
new file mode 100644
index 000000000..cd168a8a3
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-wincred/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "cargo-credential-wincred"
+version = "0.2.0"
+edition = "2021"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/rust-lang/cargo"
+description = "A Cargo credential process that stores tokens with Windows Credential Manager."
+
+[dependencies]
+cargo-credential.workspace = true
+
+[target.'cfg(windows)'.dependencies.windows-sys]
+features = ["Win32_Foundation", "Win32_Security_Credentials"]
+workspace = true
diff --git a/src/tools/cargo/credential/cargo-credential-wincred/src/main.rs b/src/tools/cargo/credential/cargo-credential-wincred/src/main.rs
new file mode 100644
index 000000000..4377172e8
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential-wincred/src/main.rs
@@ -0,0 +1,122 @@
+//! Cargo registry windows credential process.
+
+#[cfg(windows)]
+mod win {
+ use cargo_credential::{Credential, Error};
+ use std::ffi::OsStr;
+ use std::os::windows::ffi::OsStrExt;
+
+ use windows_sys::core::PWSTR;
+ use windows_sys::Win32::Foundation::ERROR_NOT_FOUND;
+ use windows_sys::Win32::Foundation::FILETIME;
+ use windows_sys::Win32::Foundation::TRUE;
+ use windows_sys::Win32::Security::Credentials::CredDeleteW;
+ use windows_sys::Win32::Security::Credentials::CredReadW;
+ use windows_sys::Win32::Security::Credentials::CredWriteW;
+ use windows_sys::Win32::Security::Credentials::CREDENTIALW;
+ use windows_sys::Win32::Security::Credentials::CRED_PERSIST_LOCAL_MACHINE;
+ use windows_sys::Win32::Security::Credentials::CRED_TYPE_GENERIC;
+
+ pub(crate) struct WindowsCredential;
+
+ /// Converts a string to a nul-terminated wide UTF-16 byte sequence.
+ fn wstr(s: &str) -> Vec<u16> {
+ let mut wide: Vec<u16> = OsStr::new(s).encode_wide().collect();
+ if wide.iter().any(|b| *b == 0) {
+ panic!("nul byte in wide string");
+ }
+ wide.push(0);
+ wide
+ }
+
+ fn target_name(registry_name: &str) -> Vec<u16> {
+ wstr(&format!("cargo-registry:{}", registry_name))
+ }
+
+ impl Credential for WindowsCredential {
+ fn name(&self) -> &'static str {
+ env!("CARGO_PKG_NAME")
+ }
+
+ fn get(&self, index_url: &str) -> Result<String, Error> {
+ let target_name = target_name(index_url);
+ let p_credential: *mut CREDENTIALW = std::ptr::null_mut() as *mut _;
+ unsafe {
+ if CredReadW(
+ target_name.as_ptr(),
+ CRED_TYPE_GENERIC,
+ 0,
+ p_credential as *mut _ as *mut _,
+ ) != TRUE
+ {
+ return Err(format!(
+ "failed to fetch token: {}",
+ std::io::Error::last_os_error()
+ )
+ .into());
+ }
+ let bytes = std::slice::from_raw_parts(
+ (*p_credential).CredentialBlob,
+ (*p_credential).CredentialBlobSize as usize,
+ );
+ String::from_utf8(bytes.to_vec())
+ .map_err(|_| "failed to convert token to UTF8".into())
+ }
+ }
+
+ fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error> {
+ let token = token.as_bytes();
+ let target_name = target_name(index_url);
+ let comment = match name {
+ Some(name) => wstr(&format!("Cargo registry token for {}", name)),
+ None => wstr("Cargo registry token"),
+ };
+ let mut credential = CREDENTIALW {
+ Flags: 0,
+ Type: CRED_TYPE_GENERIC,
+ TargetName: target_name.as_ptr() as PWSTR,
+ Comment: comment.as_ptr() as PWSTR,
+ LastWritten: FILETIME {
+ dwLowDateTime: 0,
+ dwHighDateTime: 0,
+ },
+ CredentialBlobSize: token.len() as u32,
+ CredentialBlob: token.as_ptr() as *mut u8,
+ Persist: CRED_PERSIST_LOCAL_MACHINE,
+ AttributeCount: 0,
+ Attributes: std::ptr::null_mut(),
+ TargetAlias: std::ptr::null_mut(),
+ UserName: std::ptr::null_mut(),
+ };
+ let result = unsafe { CredWriteW(&mut credential, 0) };
+ if result != TRUE {
+ let err = std::io::Error::last_os_error();
+ return Err(format!("failed to store token: {}", err).into());
+ }
+ Ok(())
+ }
+
+ fn erase(&self, index_url: &str) -> Result<(), Error> {
+ let target_name = target_name(index_url);
+ let result = unsafe { CredDeleteW(target_name.as_ptr(), CRED_TYPE_GENERIC, 0) };
+ if result != TRUE {
+ let err = std::io::Error::last_os_error();
+ if err.raw_os_error() == Some(ERROR_NOT_FOUND as i32) {
+ eprintln!("not currently logged in to `{}`", index_url);
+ return Ok(());
+ }
+ return Err(format!("failed to remove token: {}", err).into());
+ }
+ Ok(())
+ }
+ }
+}
+
+#[cfg(not(windows))]
+use cargo_credential::UnsupportedCredential as WindowsCredential;
+#[cfg(windows)]
+use win::WindowsCredential;
+
+fn main() {
+ cargo_credential::main(WindowsCredential);
+}
diff --git a/src/tools/cargo/credential/cargo-credential/Cargo.toml b/src/tools/cargo/credential/cargo-credential/Cargo.toml
new file mode 100644
index 000000000..2addaf5af
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "cargo-credential"
+version = "0.2.0"
+edition = "2021"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/rust-lang/cargo"
+description = "A library to assist writing Cargo credential helpers."
+
+[dependencies]
diff --git a/src/tools/cargo/credential/cargo-credential/README.md b/src/tools/cargo/credential/cargo-credential/README.md
new file mode 100644
index 000000000..53dc8e6b7
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential/README.md
@@ -0,0 +1,41 @@
+# cargo-credential
+
+This package is a library to assist writing a Cargo credential helper, which
+provides an interface to store tokens for authorizing access to a registry
+such as https://crates.io/.
+
+Documentation about credential processes may be found at
+https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#credential-process
+
+Example implementations may be found at
+https://github.com/rust-lang/cargo/tree/master/credential
+
+## Usage
+
+Create a Cargo project with this as a dependency:
+
+```toml
+# Add this to your Cargo.toml:
+
+[dependencies]
+cargo-credential = "0.1"
+```
+
+And then include a `main.rs` binary which implements the `Credential` trait, and calls
+the `main` function which will call the appropriate method of the trait:
+
+```rust
+// src/main.rs
+
+use cargo_credential::{Credential, Error};
+
+struct MyCredential;
+
+impl Credential for MyCredential {
+ /// implement trait methods here...
+}
+
+fn main() {
+ cargo_credential::main(MyCredential);
+}
+```
diff --git a/src/tools/cargo/credential/cargo-credential/src/lib.rs b/src/tools/cargo/credential/cargo-credential/src/lib.rs
new file mode 100644
index 000000000..c75172242
--- /dev/null
+++ b/src/tools/cargo/credential/cargo-credential/src/lib.rs
@@ -0,0 +1,106 @@
+//! Helper library for writing Cargo credential processes.
+//!
+//! A credential process should have a `struct` that implements the `Credential` trait.
+//! The `main` function should be called with an instance of that struct, such as:
+//!
+//! ```rust,ignore
+//! fn main() {
+//! cargo_credential::main(MyCredential);
+//! }
+//! ```
+//!
+//! This will determine the action to perform (get/store/erase) by looking at
+//! the CLI arguments for the first argument that does not start with `-`. It
+//! will then call the corresponding method of the trait to perform the
+//! requested action.
+
+pub type Error = Box<dyn std::error::Error>;
+
+pub trait Credential {
+ /// Returns the name of this credential process.
+ fn name(&self) -> &'static str;
+
+ /// Retrieves a token for the given registry.
+ fn get(&self, index_url: &str) -> Result<String, Error>;
+
+ /// Stores the given token for the given registry.
+ fn store(&self, index_url: &str, token: &str, name: Option<&str>) -> Result<(), Error>;
+
+ /// Removes the token for the given registry.
+ ///
+ /// If the user is not logged in, this should print a message to stderr if
+ /// possible indicating that the user is not currently logged in, and
+ /// return `Ok`.
+ fn erase(&self, index_url: &str) -> Result<(), Error>;
+}
+
+pub struct UnsupportedCredential;
+
+impl Credential for UnsupportedCredential {
+ fn name(&self) -> &'static str {
+ "unsupported"
+ }
+
+ fn get(&self, _index_url: &str) -> Result<String, Error> {
+ Err("unsupported".into())
+ }
+
+ fn store(&self, _index_url: &str, _token: &str, _name: Option<&str>) -> Result<(), Error> {
+ Err("unsupported".into())
+ }
+
+ fn erase(&self, _index_url: &str) -> Result<(), Error> {
+ Err("unsupported".into())
+ }
+}
+
+/// Runs the credential interaction by processing the command-line and
+/// environment variables.
+pub fn main(credential: impl Credential) {
+ let name = credential.name();
+ if let Err(e) = doit(credential) {
+ eprintln!("{} error: {}", name, e);
+ std::process::exit(1);
+ }
+}
+
+fn env(name: &str) -> Result<String, Error> {
+ std::env::var(name).map_err(|_| format!("environment variable `{}` is not set", name).into())
+}
+
+fn doit(credential: impl Credential) -> Result<(), Error> {
+ let which = std::env::args()
+ .skip(1)
+ .skip_while(|arg| arg.starts_with('-'))
+ .next()
+ .ok_or_else(|| "first argument must be the {action}")?;
+ let index_url = env("CARGO_REGISTRY_INDEX_URL")?;
+ let name = std::env::var("CARGO_REGISTRY_NAME_OPT").ok();
+ let result = match which.as_ref() {
+ "get" => credential.get(&index_url).and_then(|token| {
+ println!("{}", token);
+ Ok(())
+ }),
+ "store" => {
+ read_token().and_then(|token| credential.store(&index_url, &token, name.as_deref()))
+ }
+ "erase" => credential.erase(&index_url),
+ _ => {
+ return Err(format!(
+ "unexpected command-line argument `{}`, expected get/store/erase",
+ which
+ )
+ .into())
+ }
+ };
+ result.map_err(|e| format!("failed to `{}` token: {}", which, e).into())
+}
+
+fn read_token() -> Result<String, Error> {
+ let mut buffer = String::new();
+ std::io::stdin().read_line(&mut buffer)?;
+ if buffer.ends_with('\n') {
+ buffer.pop();
+ }
+ Ok(buffer)
+}