Implement passwords logic for windows
This commit is contained in:
parent
52f598b052
commit
ef779114ef
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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(""))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue