Skip to content
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

Allow interoperability with 3rd-party conventions for keyring item names #67

Closed
wants to merge 9 commits into from
4 changes: 0 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ repository = "https://github.com/hwchen/keyring-rs.git"
version = "0.10.4"
edition = "2018"

[features]
default = []
macos-specify-keychain = []

[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "2.4.2"

Expand Down
116 changes: 116 additions & 0 deletions src/attrs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
Every platform's secure storage system keeps a set of attributes with
each stored item. Which attributes are allowed can vary, as can which
of the attributes are required and which are used to identify the item.

The attribute model supported by this crate is that each item has only
two attributes: username and service, and they are used together to
uniquely identify the item.

The mismatch between this crate's attribute model and the underlying
platform's attribute model can lead to incompatibility with 3rd-party
applications whose attribute model, while consistent with the underlying
platform model, may be more or less fine-grained than this crate's model.

For example:

* On Windows, generic credential are identified by an arbitrary string,
and that string may not be constructed by a third party application
the same way this crate constructs it from username and service.
* On Linux, additional attributes can be used by 3rd parties to produce
credentials identified my more than just the two attributes this crate
uses by default.

Thus, to provide interoperability with 3rd party credential clients,
we provide a way for clients of this crate to override this crate's
default algorithm for how the username and service are combined so as to
produce the platform-specific attributes that identify each item.
*/

use std::collections::HashMap;

#[derive(Debug)]
pub enum Platform {
Linux,
Windows,
MacOs,
}

#[derive(Debug)]
pub struct LinuxIdentity {
pub attributes: HashMap<String, String>,
pub label: String,
}

impl LinuxIdentity {
pub fn attributes(&self) -> HashMap<&str, &str> {
self.attributes
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect()
}

pub fn label(&self) -> &str {
self.label.as_str()
}
}

#[derive(Debug)]
pub struct WinIdentity {
pub target_name: String,
pub username: String,
}

#[derive(Debug)]
pub struct MacIdentity {
pub service: String,
pub account: String,
}

#[derive(Debug)]
pub enum PlatformIdentity {
Linux(LinuxIdentity),
Win(WinIdentity),
Mac(MacIdentity),
}

impl PlatformIdentity {
pub fn matches_platform(&self, os: &Platform) -> bool {
match self {
PlatformIdentity::Linux(_) => matches!(os, Platform::Linux),
PlatformIdentity::Mac(_) => matches!(os, Platform::MacOs),
PlatformIdentity::Win(_) => matches!(os, Platform::Windows),
}
}
}

// TODO: Make this a Fn trait so we can accept closures
pub type IdentityMapper = fn(&Platform, &str, &str) -> PlatformIdentity;

pub fn default_identity_mapper(
platform: Platform,
service: &str,
username: &str,
) -> PlatformIdentity {
match platform {
Platform::Linux => PlatformIdentity::Linux(LinuxIdentity {
attributes: HashMap::from([
("service".to_string(), service.to_string()),
("username".to_string(), username.to_string()),
("application".to_string(), "rust-keyring".to_string()),
]),
label: format!("Password for service '{}', user '{}'", service, username),
}),
Platform::Windows => PlatformIdentity::Win(WinIdentity {
// Note: default concatenation of user and service name is
// needed because windows identity is on target_name only
// See issue here: https://github.com/jaraco/keyring/issues/47
target_name: format!("{}.{}", username, service),
username: username.to_string(),
}),
Platform::MacOs => PlatformIdentity::Mac(MacIdentity {
service: service.to_string(),
account: username.to_string(),
}),
}
}
10 changes: 9 additions & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub enum KeyringError {
NoBackendFound,
NoPasswordFound,
Parse(FromUtf8Error),
BadPlatformMapValue,
}

impl fmt::Display for KeyringError {
Expand All @@ -34,7 +35,11 @@ impl fmt::Display for KeyringError {
KeyringError::WindowsVaultError => write!(f, "Windows Vault Error"),
KeyringError::NoBackendFound => write!(f, "Keyring error: No Backend Found"),
KeyringError::NoPasswordFound => write!(f, "Keyring Error: No Password Found"),
KeyringError::Parse(ref err) => write!(f, "Keyring Parse Error: {}", err),
KeyringError::Parse(ref err) => write!(f, "Password Parse Error: {}", err),
KeyringError::BadPlatformMapValue => write!(
f,
"Keyring Error: Custom IdentityMapper value doesn't match this platform"
),
}
}
}
Expand All @@ -55,6 +60,9 @@ impl error::Error for KeyringError {
KeyringError::NoBackendFound => "No Backend Found",
KeyringError::NoPasswordFound => "No Password Found",
KeyringError::Parse(ref err) => err.description(),
KeyringError::BadPlatformMapValue => {
"Custom IdentityMapper value doesn't match this platform"
}
}
}

Expand Down
104 changes: 86 additions & 18 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,68 @@
//!
//! Allows for setting and getting passwords on Linux, OSX, and Windows

// Configure for Linux
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::Keyring;

// Configure for Windows
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
pub use windows::Keyring;

// Configure for OSX
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
pub use macos::Keyring;

mod attrs;
mod error;

use crate::error::KeyringError::BadPlatformMapValue;
pub use attrs::{IdentityMapper, Platform, PlatformIdentity};
pub use error::{KeyringError, Result};

// compile-time Platform known at runtime
fn platform() -> Platform {
#[cfg(target_os = "linux")]
return Platform::Linux;
#[cfg(target_os = "windows")]
return Platform::Windows;
#[cfg(target_os = "macos")]
return Platform::MacOs;
}

// Platform-specific implementations
#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
#[cfg_attr(target_os = "macos", path = "macos.rs")]
mod platform;

#[derive(Debug)]
pub struct Keyring {
map: PlatformIdentity,
}

impl Keyring {
pub fn new(service: &str, username: &str) -> Keyring {
Keyring {
map: attrs::default_identity_mapper(platform(), service, username),
}
}

pub fn new_with_mapper(
service: &str,
username: &str,
mapper: IdentityMapper,
) -> Result<Keyring> {
let os = platform();
let map = mapper(&os, service, username);
if map.matches_platform(&os) {
Ok(Keyring { map })
} else {
Err(BadPlatformMapValue)
}
}

pub fn set_password(&self, password: &str) -> Result<()> {
platform::set_password(&self.map, password)
}

pub fn get_password(&self) -> Result<String> {
platform::get_password(&self.map)
}

pub fn delete_password(&self) -> Result<()> {
platform::delete_password(&self.map)
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -33,6 +74,18 @@ mod tests {
static TEST_ASCII_PASSWORD: &'static str = "my_password";
static TEST_NON_ASCII_PASSWORD: &'static str = "大根";

#[test]
#[serial]
fn test_empty_keyring() {
let service = generate_random_string();
let username = generate_random_string();
let keyring = Keyring::new(&service, &username);
assert!(
keyring.get_password().is_err(),
"Read a password from a non-existent platform item"
)
}

#[test]
#[serial]
fn test_empty_password_input() {
Expand Down Expand Up @@ -75,4 +128,19 @@ mod tests {
"Able to read a deleted password"
)
}

// TODO: write tests for custom mappers.
// This might be better done in a separate test file.

// utility
fn generate_random_string() -> String {
// from the Rust Cookbook:
// https://rust-lang-nursery.github.io/rust-cookbook/algorithms/randomness.html
use rand::{distributions::Alphanumeric, thread_rng, Rng};
thread_rng()
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect()
}
}
Loading