Implement passwords logic for windows

This commit is contained in:
Hinton 2022-03-03 10:53:21 +01:00
parent 52f598b052
commit ef779114ef
10 changed files with 333 additions and 80 deletions

View File

@ -76,9 +76,11 @@ dependencies = [
"napi",
"napi-build",
"napi-derive",
"scopeguard",
"security-framework",
"security-framework-sys",
"tokio",
"widestring",
"windows 0.32.0",
]
@ -401,6 +403,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "widestring"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983"
[[package]]
name = "winapi"
version = "0.3.9"

View File

@ -15,17 +15,23 @@ tokio = { version = "1.17.0", features = ["full"] }
anyhow = "1.0"
napi = { version = "2", features=["async"] }
napi-derive = "2"
scopeguard = "1.1.0"
[build-dependencies]
napi-build = "1"
[target.'cfg(windows)'.dependencies]
widestring = "0.5.1"
[target.'cfg(windows)'.dependencies.windows]
version = "0.32.0"
features = [
"alloc",
"Foundation",
"Security_Credentials_UI",
"Storage_Streams",
"Win32_Foundation",
"Win32_Security_Credentials",
"Win32_System_WinRT",
]

View File

@ -1,6 +1,6 @@
#[cfg_attr(target_os = "linux", path = "unix.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
#[cfg_attr(target_os = "macos", path = "mac/mod.rs")]
#[cfg_attr(target_os = "macos", path = "macos/mod.rs")]
mod imp;
pub async fn available() -> bool {

View File

@ -1,66 +0,0 @@
use windows::core::*;
use windows::Foundation::IAsyncOperation;
use windows::Security::Credentials::UI::*;
use windows::Win32::Foundation::HWND;
use windows::Win32::System::WinRT::IUserConsentVerifierInterop;
pub async fn available() -> bool {
let event = UserConsentVerifier::CheckAvailabilityAsync();
let result = match event {
Err(_) => return false,
Ok(t) => t.await,
};
match result {
Err(_) => false,
Ok(t) => match t {
UserConsentVerifierAvailability::Available => true,
UserConsentVerifierAvailability::DeviceBusy => true,
_ => false,
},
}
}
pub async fn verify(
message: &str,
window_handle: isize,
) -> std::result::Result<bool, Box<dyn std::error::Error + Send + Sync>> {
let interop: IUserConsentVerifierInterop =
match factory::<UserConsentVerifier, IUserConsentVerifierInterop>() {
Ok(i) => i,
Err(e) => return Err(e.into()),
};
let window = HWND(window_handle);
let operation: Result<IAsyncOperation<UserConsentVerificationResult>> =
unsafe { interop.RequestVerificationForWindowAsync(window, message) };
let result = match operation {
Ok(r) => r.await,
Err(e) => return Err(e.into()),
};
match result {
Err(e) => return Err(e.into()),
Ok(t) => match t {
UserConsentVerificationResult::Verified => Ok(true),
_ => Ok(false),
},
}
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn available() {
// TODO Mock!
// assert_eq!(true, super::available().await)
}
#[test]
fn verify() {
// TODO Mock!
//assert_eq!(true, super::verify("test", 0))
}
}

View File

@ -0,0 +1,138 @@
use windows::core::*;
use windows::Foundation::IAsyncOperation;
use windows::Security::Credentials::UI::*;
//use windows::Win32::Foundation::HWND;
//use windows::Win32::System::WinRT::IUserConsentVerifierInterop;
use anyhow::anyhow;
use windows::Security::Credentials::*;
use windows::Storage::Streams::{DataWriter, IBuffer};
use windows::Win32::System::WinRT::IBufferByteAccess;
pub async fn available() -> bool {
let event = UserConsentVerifier::CheckAvailabilityAsync();
let result = match event {
Err(_) => return false,
Ok(t) => t.await,
};
match result {
Err(_) => false,
Ok(t) => match t {
UserConsentVerifierAvailability::Available => true,
UserConsentVerifierAvailability::DeviceBusy => true,
_ => false,
},
}
}
async fn createEncryptionKey<'a>(name: &'a str, challenge: &'a str) -> anyhow::Result<&'a str> {
let result = KeyCredentialManager::OpenAsync(name)?.await?;
let result = match result.Status()? {
KeyCredentialStatus::NotFound => {
KeyCredentialManager::RequestCreateAsync(
name,
KeyCredentialCreationOption::ReplaceExisting,
)?
.await?
}
KeyCredentialStatus::Success => result,
_ => return Err(anyhow!("Error")),
};
let credential = result.Credential()?;
let challenge_writer: DataWriter = DataWriter::new()?;
match challenge_writer.WriteString(challenge) {
Err(e) => return Err(e.into()),
Ok(_) => (),
};
let challenge_buffer = challenge_writer.DetachBuffer()?;
let sign_result: KeyCredentialOperationResult =
credential.RequestSignAsync(challenge_buffer)?.await?;
Ok("hi")
}
pub async fn verify(
message: &str,
window_handle: isize,
) -> std::result::Result<bool, Box<dyn std::error::Error + Send + Sync>> {
createEncryptionKey("test123", "random").await;
//KeyCredentialManager::RequestCreateAsync("test", KeyCredentialCreationOption::ReplaceExisting);
let i_open_key_result: IAsyncOperation<KeyCredentialRetrievalResult> =
KeyCredentialManager::OpenAsync("test").unwrap();
let open_key_result: KeyCredentialRetrievalResult = i_open_key_result.get().unwrap();
let user_key: KeyCredential = open_key_result.Credential().unwrap();
let data_writer: DataWriter = DataWriter::new().unwrap();
data_writer.WriteString("test123");
let buffer = data_writer.DetachBuffer().unwrap();
//let object = Buffer::Create(5)?;
/* let bytes: *mut u8 = unsafe { object.cast::<IBufferByteAccess>()?.Buffer()? };
let bytes = unsafe { core::slice::from_raw_parts_mut(bytes, 5) };
bytes.copy_from_slice(&[0xAA, 0xBB, 0xBA, 0xCC, 0xBB]);
*/
let sign_result: KeyCredentialOperationResult =
user_key.RequestSignAsync(buffer).unwrap().await.unwrap();
// TODO: Add logic for focusin on the dialog prompt.
let object: IBuffer = sign_result.Result().unwrap();
let bytes: *const u8 = unsafe { object.cast::<IBufferByteAccess>()?.Buffer()? };
let bytes = unsafe { core::slice::from_raw_parts(bytes, object.Length().unwrap() as usize) };
println!("{:?}", bytes);
/*
let interop: IUserConsentVerifierInterop =
match factory::<UserConsentVerifier, IUserConsentVerifierInterop>() {
Ok(i) => i,
Err(e) => return Err(e.into()),
};
let window = HWND(window_handle);
let operation: Result<IAsyncOperation<UserConsentVerificationResult>> =
unsafe { interop.RequestVerificationForWindowAsync(window, message) };
let result = match operation {
Ok(r) => r.await,
Err(e) => return Err(e.into()),
};
match result {
Err(e) => return Err(e.into()),
Ok(t) => match t {
UserConsentVerificationResult::Verified => Ok(true),
_ => Ok(false),
},
}
*/
Ok(true)
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn available() {
// TODO Mock!
// assert_eq!(true, super::available().await)
}
#[tokio::test]
async fn verify() {
// TODO Mock!
//assert_eq!(true, super::verify("test", 0).await.unwrap())
}
}

View File

@ -38,29 +38,46 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> {
*/
#[allow(dead_code)]
#[napi]
mod passwords {
/// Fetch the stored password from the keychain.
#[napi]
pub async fn get_password(service: String, account: String) -> napi::Result<String> {
super::password::get_password(service.as_str(), account.as_str()).await;
return Ok(String::from("123"));
super::password::get_password(service.as_str(), account.as_str())
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Save the password to the keychain. Adds an entry if none exists otherwise updates the existing entry.
#[napi]
pub async fn set_password(service: String, account: String, password: String) {}
pub async fn set_password(
service: String,
account: String,
password: String,
) -> napi::Result<()> {
super::password::set_password(service.as_str(), account.as_str(), password.as_str())
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Delete the stored password from the keychain.
#[napi]
pub async fn delete_password(service: String, account: String) {}
pub async fn delete_password(service: String, account: String) -> napi::Result<()> {
super::password::delete_password(service.as_str(), account.as_str())
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
}
#[allow(dead_code)]
#[napi]
mod biometrics {
/// Verify user presence.
#[napi]
pub async fn prompt(message: String) {}
pub async fn prompt(message: String) {
println!("{}", message);
}
/// Enable biometric for the specific account, stores the encrypted password in keychain on macOS,
/// gnome keyring on Unix, and returns an encrypted string on Windows.
@ -70,16 +87,22 @@ mod biometrics {
password: String,
message: String,
) -> napi::Result<String> {
println!("{}, {}, {}", account, password, message);
Ok(String::from(""))
}
#[napi]
/// Remove the stored biometric key for the specified account.
pub async fn disable(account: String) {}
pub async fn disable(account: String) {
println!("{}", account);
}
/// Decrypt the secured password after verifying the user presense using biometric.
#[napi]
pub async fn decrypt(account: String, encrypted_password: String) -> napi::Result<String> {
println!("{}, {}", account, encrypted_password);
Ok(String::from(""))
}
}

View File

@ -10,13 +10,11 @@ pub async fn get_password<'a>(service: &str, account: &str) -> Result<String> {
}
pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> {
set_generic_password(&service, &account, password.as_bytes())
.map_err(anyhow::Error::msg)
set_generic_password(&service, &account, password.as_bytes()).map_err(anyhow::Error::msg)
}
pub async fn delete_password(service: &str, account: &str) -> Result<()> {
delete_generic_password(&service, &account)
.map_err(anyhow::Error::msg)
delete_generic_password(&service, &account).map_err(anyhow::Error::msg)
}
#[cfg(test)]
@ -25,8 +23,17 @@ mod tests {
#[tokio::test]
async fn test() {
set_password("BitwardenTest", "BitwardenTest", "Random").await.unwrap();
assert_eq!("Random", get_password("BitwardenTest", "BitwardenTest").await.unwrap());
delete_password("BitwardenTest", "BitwardenTest").await.unwrap();
set_password("BitwardenTest", "BitwardenTest", "Random")
.await
.unwrap();
assert_eq!(
"Random",
get_password("BitwardenTest", "BitwardenTest")
.await
.unwrap()
);
delete_password("BitwardenTest", "BitwardenTest")
.await
.unwrap();
}
}

View File

@ -0,0 +1,137 @@
use anyhow::{anyhow, Result};
use widestring::U16CString;
use windows::Win32::{
Foundation::{GetLastError, FILETIME, PWSTR},
Security::Credentials::{
CredDeleteW, CredFree, CredReadW, CredWriteW, CREDENTIALW, CRED_FLAGS,
CRED_PERSIST_ENTERPRISE, CRED_TYPE_GENERIC,
},
};
const CRED_FLAGS_NONE: u32 = 0;
pub async 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!(unsafe { GetLastError() }.0.to_string()));
}
// Keytar compatible
let password = unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
(*credential).CredentialBlob,
(*credential).CredentialBlobSize as usize,
))
};
/*
// Fetches credentials set the regular way
let password = unsafe {
U16String::from_ptr(
(*credential).CredentialBlob as *const u16,
(*credential).CredentialBlobSize as usize / 2,
)
.to_string_lossy()
};
*/
Ok(String::from(password))
}
pub async 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,
};
// Keytar compatible
let credential = std::ffi::CString::new(password)?;
let credential_len = password.len() as u32;
/*
// Written data would be readable from more common sources
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 async 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)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test() {
set_password("BitwardenTest", "BitwardenTest", "Random")
.await
.unwrap();
assert_eq!(
"Random",
get_password("BitwardenTest", "BitwardenTest")
.await
.unwrap()
);
delete_password("BitwardenTest", "BitwardenTest")
.await
.unwrap();
}
}