use keyring::credential::{Credential, CredentialApi, CredentialBuilderApi};
use std::fs::File;
use std::io::{Read, Write};
use std::path::PathBuf;
use url::form_urlencoded::Serializer;
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FlatfileCredentialBuilder(PathBuf);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FlatfileCredential(PathBuf);
impl FlatfileCredentialBuilder {
pub fn new() -> keyring::Result<Self> {
let dir = dirs::config_dir()
.ok_or(keyring::Error::NoEntry)?
.join("warg")
.join("keyring");
Self::new_with_basepath(dir)
}
pub fn new_with_basepath(basepath: PathBuf) -> keyring::Result<Self> {
std::fs::create_dir_all(basepath.as_path())
.map_err(|e| keyring::Error::PlatformFailure(Box::new(e)))?;
#[cfg(unix)]
std::fs::set_permissions(basepath.as_path(), std::fs::Permissions::from_mode(0o700))
.map_err(|e| keyring::Error::PlatformFailure(Box::new(e)))?;
Ok(Self(basepath))
}
}
impl CredentialBuilderApi for FlatfileCredentialBuilder {
fn build(
&self,
target: Option<&str>,
service: &str,
user: &str,
) -> keyring::Result<Box<Credential>> {
let mut serializer = Serializer::new(String::new());
if let Some(target) = target {
serializer.append_pair("target", target);
}
serializer.append_pair("service", service);
serializer.append_pair("user", user);
let filename = serializer.finish();
let path = self.0.join(filename);
Ok(Box::new(FlatfileCredential(path)))
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl CredentialApi for FlatfileCredential {
fn set_password(&self, password: &str) -> keyring::Result<()> {
self.set_secret(password.as_bytes())
}
fn set_secret(&self, password: &[u8]) -> keyring::Result<()> {
let mut options = std::fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
options.mode(0o600);
let mut f = options
.open(self.0.as_path())
.map_err(|e| keyring::Error::PlatformFailure(Box::new(e)))?;
f.write_all(password)
.map_err(|e| keyring::Error::PlatformFailure(Box::new(e)))?;
Ok(())
}
fn get_password(&self) -> keyring::Result<String> {
String::from_utf8(self.get_secret()?)
.map_err(|e| keyring::Error::BadEncoding(e.into_bytes()))
}
fn get_secret(&self) -> keyring::Result<Vec<u8>> {
match File::open(self.0.as_path()) {
Ok(mut f) => {
let mut buf = Vec::new();
f.read_to_end(&mut buf)
.map_err(|e| keyring::Error::PlatformFailure(Box::new(e)))?;
Ok(buf)
}
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Err(keyring::Error::NoEntry)
} else {
Err(keyring::Error::PlatformFailure(Box::new(e)))
}
}
}
}
fn delete_credential(&self) -> keyring::Result<()> {
match std::fs::remove_file(self.0.as_path()) {
Ok(()) => Ok(()),
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
Err(keyring::Error::NoEntry)
} else {
Err(keyring::Error::PlatformFailure(Box::new(e)))
}
}
}
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[test]
fn test_smoke() {
let basepath = tempfile::tempdir().unwrap();
let keyring =
FlatfileCredentialBuilder::new_with_basepath(basepath.as_ref().to_owned()).unwrap();
let cred = keyring.build(None, "service1", "user1").unwrap();
assert!(matches!(
cred.get_password().unwrap_err(),
keyring::Error::NoEntry
));
cred.set_password("correct horse battery staple").unwrap();
assert_eq!(cred.get_password().unwrap(), "correct horse battery staple");
let _dirattr = std::fs::metadata(basepath.as_ref()).unwrap();
#[cfg(unix)]
assert_eq!(_dirattr.permissions().mode() & 0o7777, 0o700);
let filepath = basepath.as_ref().join("service=service1&user=user1");
let _fileattr = std::fs::metadata(filepath.as_path()).unwrap();
#[cfg(unix)]
assert_eq!(_fileattr.permissions().mode() & 0o7777, 0o600);
cred.delete_credential().unwrap();
assert!(matches!(
cred.get_password().unwrap_err(),
keyring::Error::NoEntry
));
}