181 lines
5.1 KiB
Rust
181 lines
5.1 KiB
Rust
use anyhow::{anyhow, Result};
|
|
use widestring::{U16CString, U16String};
|
|
use windows::Win32::{
|
|
Foundation::{GetLastError, ERROR_NOT_FOUND, FILETIME, PWSTR, WIN32_ERROR},
|
|
Security::Credentials::{
|
|
CredDeleteW, CredFree, CredReadW, CredWriteW, CREDENTIALW, CRED_FLAGS,
|
|
CRED_PERSIST_ENTERPRISE, CRED_TYPE_GENERIC,
|
|
},
|
|
};
|
|
|
|
const CRED_FLAGS_NONE: u32 = 0;
|
|
|
|
pub fn get_password<'a>(service: &str, account: &str) -> Result<String> {
|
|
let target_name = U16CString::from_str(target_name(service, account))?;
|
|
|
|
let mut credential: *mut CREDENTIALW = std::ptr::null_mut();
|
|
let credential_ptr = &mut credential;
|
|
|
|
let result = unsafe {
|
|
CredReadW(
|
|
PWSTR(target_name.as_ptr()),
|
|
CRED_TYPE_GENERIC.0,
|
|
CRED_FLAGS_NONE,
|
|
credential_ptr,
|
|
)
|
|
};
|
|
|
|
scopeguard::defer!({
|
|
unsafe { CredFree(credential as *mut _) };
|
|
});
|
|
|
|
if !result.as_bool() {
|
|
return Err(anyhow!(convert_error(unsafe { GetLastError() })));
|
|
}
|
|
|
|
let password = unsafe {
|
|
U16String::from_ptr(
|
|
(*credential).CredentialBlob as *const u16,
|
|
(*credential).CredentialBlobSize as usize / 2,
|
|
)
|
|
.to_string_lossy()
|
|
};
|
|
|
|
Ok(String::from(password))
|
|
}
|
|
|
|
// Remove this after sufficient releases
|
|
pub fn get_password_keytar<'a>(service: &str, account: &str) -> Result<String> {
|
|
let target_name = U16CString::from_str(target_name(service, account))?;
|
|
|
|
let mut credential: *mut CREDENTIALW = std::ptr::null_mut();
|
|
let credential_ptr = &mut credential;
|
|
|
|
let result = unsafe {
|
|
CredReadW(
|
|
PWSTR(target_name.as_ptr()),
|
|
CRED_TYPE_GENERIC.0,
|
|
CRED_FLAGS_NONE,
|
|
credential_ptr,
|
|
)
|
|
};
|
|
|
|
scopeguard::defer!({
|
|
unsafe { CredFree(credential as *mut _) };
|
|
});
|
|
|
|
if !result.as_bool() {
|
|
return Err(anyhow!(unsafe { GetLastError() }.0.to_string()));
|
|
}
|
|
|
|
let password = unsafe {
|
|
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
|
|
(*credential).CredentialBlob,
|
|
(*credential).CredentialBlobSize as usize,
|
|
))
|
|
};
|
|
|
|
Ok(String::from(password))
|
|
}
|
|
|
|
pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> {
|
|
let target_name = U16CString::from_str(target_name(service, account))?;
|
|
let user_name = U16CString::from_str(account)?;
|
|
let last_written = FILETIME {
|
|
dwLowDateTime: 0,
|
|
dwHighDateTime: 0,
|
|
};
|
|
|
|
let credential = U16CString::from_str(password)?;
|
|
let credential_len = password.len() as u32 * 2;
|
|
|
|
let credential = CREDENTIALW {
|
|
Flags: CRED_FLAGS(CRED_FLAGS_NONE),
|
|
Type: CRED_TYPE_GENERIC,
|
|
TargetName: PWSTR(target_name.as_ptr()),
|
|
Comment: PWSTR::default(),
|
|
LastWritten: last_written,
|
|
CredentialBlobSize: credential_len,
|
|
CredentialBlob: credential.as_ptr() as *mut u8,
|
|
Persist: CRED_PERSIST_ENTERPRISE,
|
|
AttributeCount: 0,
|
|
Attributes: std::ptr::null_mut(),
|
|
TargetAlias: PWSTR::default(),
|
|
UserName: PWSTR(user_name.as_ptr()),
|
|
};
|
|
|
|
let result = unsafe { CredWriteW(&credential, 0) };
|
|
if !result.as_bool() {
|
|
return Err(anyhow!(unsafe { GetLastError() }.0.to_string()));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn delete_password(service: &str, account: &str) -> Result<()> {
|
|
let target_name = U16CString::from_str(target_name(service, account))?;
|
|
|
|
unsafe {
|
|
CredDeleteW(
|
|
PWSTR(target_name.as_ptr()),
|
|
CRED_TYPE_GENERIC.0,
|
|
CRED_FLAGS_NONE,
|
|
)
|
|
.ok()?
|
|
};
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn target_name(service: &str, account: &str) -> String {
|
|
format!("{}/{}", service, account)
|
|
}
|
|
|
|
// Convert the internal WIN32 errors to descriptive messages
|
|
fn convert_error(code: WIN32_ERROR) -> String {
|
|
match code {
|
|
ERROR_NOT_FOUND => String::from("Password not found."),
|
|
_ => code.0.to_string(),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test() {
|
|
scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({}););
|
|
set_password("BitwardenTest", "BitwardenTest", "Random").unwrap();
|
|
assert_eq!(
|
|
"Random",
|
|
get_password("BitwardenTest", "BitwardenTest").unwrap()
|
|
);
|
|
delete_password("BitwardenTest", "BitwardenTest").unwrap();
|
|
|
|
// Ensure password is deleted
|
|
match get_password("BitwardenTest", "BitwardenTest") {
|
|
Ok(_) => panic!("Got a result"),
|
|
Err(e) => assert_eq!("Password not found.", e.to_string()),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_password_keytar() {
|
|
scopeguard::defer!(delete_password("BitwardenTest", "BitwardenTest").unwrap_or({}););
|
|
keytar::set_password("BitwardenTest", "BitwardenTest", "HelloFromKeytar").unwrap();
|
|
assert_eq!(
|
|
"HelloFromKeytar",
|
|
get_password_keytar("BitwardenTest", "BitwardenTest").unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_error_no_password() {
|
|
match get_password("BitwardenTest", "BitwardenTest") {
|
|
Ok(_) => panic!("Got a result"),
|
|
Err(e) => assert_eq!("Password not found.", e.to_string()),
|
|
}
|
|
}
|
|
}
|