mirror of
https://github.com/bitwarden/browser
synced 2025-12-18 17:23:37 +00:00
[PM-8789] Move desktop_native into subcrate (#9682)
* Move desktop_native into subcrate * Add publish = false to crates
This commit is contained in:
51
apps/desktop/desktop_native/core/Cargo.toml
Normal file
51
apps/desktop/desktop_native/core/Cargo.toml
Normal file
@@ -0,0 +1,51 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
name = "desktop_core"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
manual_test = []
|
||||
|
||||
[dependencies]
|
||||
aes = "=0.8.4"
|
||||
anyhow = "=1.0.86"
|
||||
arboard = { version = "=3.4.0", default-features = false, features = [
|
||||
"wayland-data-control",
|
||||
] }
|
||||
base64 = "=0.22.1"
|
||||
cbc = { version = "=0.1.2", features = ["alloc"] }
|
||||
rand = "=0.8.5"
|
||||
retry = "=2.0.0"
|
||||
scopeguard = "=1.2.0"
|
||||
sha2 = "=0.10.8"
|
||||
thiserror = "=1.0.61"
|
||||
typenum = "=1.17.0"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
widestring = "=1.1.0"
|
||||
windows = { version = "=0.57.0", features = [
|
||||
"Foundation",
|
||||
"Security_Credentials_UI",
|
||||
"Security_Cryptography",
|
||||
"Storage_Streams",
|
||||
"Win32_Foundation",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
|
||||
[target.'cfg(windows)'.dev-dependencies]
|
||||
keytar = "=0.1.6"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "=0.9.4"
|
||||
security-framework = "=2.11.0"
|
||||
security-framework-sys = "=2.11.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
gio = "=0.19.5"
|
||||
libsecret = "=0.5.0"
|
||||
38
apps/desktop/desktop_native/core/src/biometric/macos.rs
Normal file
38
apps/desktop/desktop_native/core/src/biometric/macos.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use crate::biometric::{KeyMaterial, OsDerivedKey};
|
||||
|
||||
/// The MacOS implementation of the biometric trait.
|
||||
pub struct Biometric {}
|
||||
|
||||
impl super::BiometricTrait for Biometric {
|
||||
fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
|
||||
fn available() -> Result<bool> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
|
||||
fn derive_key_material(_iv_str: Option<&str>) -> Result<OsDerivedKey> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
|
||||
fn get_biometric_secret(
|
||||
_service: &str,
|
||||
_account: &str,
|
||||
_key_material: Option<KeyMaterial>,
|
||||
) -> Result<String> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
|
||||
fn set_biometric_secret(
|
||||
_service: &str,
|
||||
_account: &str,
|
||||
_secret: &str,
|
||||
_key_material: Option<super::KeyMaterial>,
|
||||
_iv_b64: &str,
|
||||
) -> Result<String> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
}
|
||||
36
apps/desktop/desktop_native/core/src/biometric/mod.rs
Normal file
36
apps/desktop/desktop_native/core/src/biometric/mod.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use anyhow::Result;
|
||||
|
||||
#[cfg_attr(target_os = "linux", path = "unix.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
mod biometric;
|
||||
|
||||
pub use biometric::Biometric;
|
||||
|
||||
pub struct KeyMaterial {
|
||||
pub os_key_part_b64: String,
|
||||
pub client_key_part_b64: Option<String>,
|
||||
}
|
||||
|
||||
pub struct OsDerivedKey {
|
||||
pub key_b64: String,
|
||||
pub iv_b64: String,
|
||||
}
|
||||
|
||||
pub trait BiometricTrait {
|
||||
fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool>;
|
||||
fn available() -> Result<bool>;
|
||||
fn derive_key_material(secret: Option<&str>) -> Result<OsDerivedKey>;
|
||||
fn set_biometric_secret(
|
||||
service: &str,
|
||||
account: &str,
|
||||
secret: &str,
|
||||
key_material: Option<KeyMaterial>,
|
||||
iv_b64: &str,
|
||||
) -> Result<String>;
|
||||
fn get_biometric_secret(
|
||||
service: &str,
|
||||
account: &str,
|
||||
key_material: Option<KeyMaterial>,
|
||||
) -> Result<String>;
|
||||
}
|
||||
38
apps/desktop/desktop_native/core/src/biometric/unix.rs
Normal file
38
apps/desktop/desktop_native/core/src/biometric/unix.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use crate::biometric::{KeyMaterial, OsDerivedKey};
|
||||
|
||||
/// The Unix implementation of the biometric trait.
|
||||
pub struct Biometric {}
|
||||
|
||||
impl super::BiometricTrait for Biometric {
|
||||
fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
|
||||
fn available() -> Result<bool> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
|
||||
fn derive_key_material(_iv_str: Option<&str>) -> Result<OsDerivedKey> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
|
||||
fn get_biometric_secret(
|
||||
_service: &str,
|
||||
_account: &str,
|
||||
_key_material: Option<KeyMaterial>,
|
||||
) -> Result<String> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
|
||||
fn set_biometric_secret(
|
||||
_service: &str,
|
||||
_account: &str,
|
||||
_secret: &str,
|
||||
_key_material: Option<KeyMaterial>,
|
||||
_iv_b64: &str,
|
||||
) -> Result<String> {
|
||||
bail!("platform not supported");
|
||||
}
|
||||
}
|
||||
442
apps/desktop/desktop_native/core/src/biometric/windows.rs
Normal file
442
apps/desktop/desktop_native/core/src/biometric/windows.rs
Normal file
@@ -0,0 +1,442 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
|
||||
use rand::RngCore;
|
||||
use retry::delay::Fixed;
|
||||
use sha2::{Digest, Sha256};
|
||||
use windows::{
|
||||
core::{factory, h, s, HSTRING},
|
||||
Foundation::IAsyncOperation,
|
||||
Security::{
|
||||
Credentials::{
|
||||
KeyCredentialCreationOption, KeyCredentialManager, KeyCredentialStatus, UI::*,
|
||||
},
|
||||
Cryptography::CryptographicBuffer,
|
||||
},
|
||||
Win32::{
|
||||
Foundation::HWND,
|
||||
System::WinRT::IUserConsentVerifierInterop,
|
||||
UI::{
|
||||
Input::KeyboardAndMouse::{
|
||||
keybd_event, GetAsyncKeyState, SetFocus, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP,
|
||||
VK_MENU,
|
||||
},
|
||||
WindowsAndMessaging::{FindWindowA, SetForegroundWindow},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
biometric::{KeyMaterial, OsDerivedKey},
|
||||
crypto::{self, CipherString},
|
||||
};
|
||||
|
||||
/// The Windows OS implementation of the biometric trait.
|
||||
pub struct Biometric {}
|
||||
|
||||
impl super::BiometricTrait for Biometric {
|
||||
fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool> {
|
||||
let h = isize::from_le_bytes(hwnd.clone().try_into().unwrap());
|
||||
let window = HWND(h);
|
||||
|
||||
// The Windows Hello prompt is displayed inside the application window. For best result we
|
||||
// should set the window to the foreground and focus it.
|
||||
set_focus(window);
|
||||
|
||||
let interop = factory::<UserConsentVerifier, IUserConsentVerifierInterop>()?;
|
||||
let operation: IAsyncOperation<UserConsentVerificationResult> =
|
||||
unsafe { interop.RequestVerificationForWindowAsync(window, &HSTRING::from(message))? };
|
||||
let result = operation.get()?;
|
||||
|
||||
match result {
|
||||
UserConsentVerificationResult::Verified => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn available() -> Result<bool> {
|
||||
let ucv_available = UserConsentVerifier::CheckAvailabilityAsync()?.get()?;
|
||||
|
||||
match ucv_available {
|
||||
UserConsentVerifierAvailability::Available => Ok(true),
|
||||
UserConsentVerifierAvailability::DeviceBusy => Ok(true), // TODO: Look into removing this and making the check more ad-hoc
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive the symmetric encryption key from the Windows Hello signature.
|
||||
///
|
||||
/// This works by signing a static challenge string with Windows Hello protected key store. The
|
||||
/// signed challenge is then hashed using SHA-256 and used as the symmetric encryption key for the
|
||||
/// Windows Hello protected keys.
|
||||
///
|
||||
/// Windows will only sign the challenge if the user has successfully authenticated with Windows,
|
||||
/// ensuring user presence.
|
||||
fn derive_key_material(challenge_str: Option<&str>) -> Result<OsDerivedKey> {
|
||||
let challenge: [u8; 16] = match challenge_str {
|
||||
Some(challenge_str) => base64_engine
|
||||
.decode(challenge_str)?
|
||||
.try_into()
|
||||
.map_err(|e: Vec<_>| anyhow!("Expect length {}, got {}", 16, e.len()))?,
|
||||
None => random_challenge(),
|
||||
};
|
||||
let bitwarden = h!("Bitwarden");
|
||||
|
||||
let result = KeyCredentialManager::RequestCreateAsync(
|
||||
&bitwarden,
|
||||
KeyCredentialCreationOption::FailIfExists,
|
||||
)?
|
||||
.get()?;
|
||||
|
||||
let result = match result.Status()? {
|
||||
KeyCredentialStatus::CredentialAlreadyExists => {
|
||||
KeyCredentialManager::OpenAsync(&bitwarden)?.get()?
|
||||
}
|
||||
KeyCredentialStatus::Success => result,
|
||||
_ => return Err(anyhow!("Failed to create key credential")),
|
||||
};
|
||||
|
||||
let challenge_buffer = CryptographicBuffer::CreateFromByteArray(&challenge)?;
|
||||
let async_operation = result.Credential()?.RequestSignAsync(&challenge_buffer)?;
|
||||
focus_security_prompt()?;
|
||||
let signature = async_operation.get()?;
|
||||
|
||||
if signature.Status()? != KeyCredentialStatus::Success {
|
||||
return Err(anyhow!("Failed to sign data"));
|
||||
}
|
||||
|
||||
let signature_buffer = signature.Result()?;
|
||||
let mut signature_value =
|
||||
windows::core::Array::<u8>::with_len(signature_buffer.Length().unwrap() as usize);
|
||||
CryptographicBuffer::CopyToByteArray(&signature_buffer, &mut signature_value)?;
|
||||
|
||||
let key = Sha256::digest(&*signature_value);
|
||||
let key_b64 = base64_engine.encode(&key);
|
||||
let iv_b64 = base64_engine.encode(&challenge);
|
||||
Ok(OsDerivedKey { key_b64, iv_b64 })
|
||||
}
|
||||
|
||||
fn set_biometric_secret(
|
||||
service: &str,
|
||||
account: &str,
|
||||
secret: &str,
|
||||
key_material: Option<KeyMaterial>,
|
||||
iv_b64: &str,
|
||||
) -> Result<String> {
|
||||
let key_material = key_material.ok_or(anyhow!(
|
||||
"Key material is required for Windows Hello protected keys"
|
||||
))?;
|
||||
|
||||
let encrypted_secret = encrypt(secret, &key_material, iv_b64)?;
|
||||
crate::password::set_password(service, account, &encrypted_secret)?;
|
||||
Ok(encrypted_secret)
|
||||
}
|
||||
|
||||
fn get_biometric_secret(
|
||||
service: &str,
|
||||
account: &str,
|
||||
key_material: Option<KeyMaterial>,
|
||||
) -> Result<String> {
|
||||
let key_material = key_material.ok_or(anyhow!(
|
||||
"Key material is required for Windows Hello protected keys"
|
||||
))?;
|
||||
|
||||
let encrypted_secret = crate::password::get_password(service, account)?;
|
||||
match CipherString::from_str(&encrypted_secret) {
|
||||
Ok(secret) => {
|
||||
// If the secret is a CipherString, it is encrypted and we need to decrypt it.
|
||||
let secret = decrypt(&secret, &key_material)?;
|
||||
return Ok(secret);
|
||||
}
|
||||
Err(_) => {
|
||||
// If the secret is not a CipherString, it is not encrypted and we can return it
|
||||
// directly.
|
||||
return Ok(encrypted_secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result<String> {
|
||||
let iv = base64_engine
|
||||
.decode(iv_b64)?
|
||||
.try_into()
|
||||
.map_err(|e: Vec<_>| anyhow!("Expected length {}, got {}", 16, e.len()))?;
|
||||
|
||||
let encrypted = crypto::encrypt_aes256(secret.as_bytes(), iv, key_material.derive_key()?)?;
|
||||
|
||||
Ok(encrypted.to_string())
|
||||
}
|
||||
|
||||
fn decrypt(secret: &CipherString, key_material: &KeyMaterial) -> Result<String> {
|
||||
if let CipherString::AesCbc256_B64 { iv, data } = secret {
|
||||
let decrypted = crypto::decrypt_aes256(&iv, &data, key_material.derive_key()?)?;
|
||||
|
||||
Ok(String::from_utf8(decrypted)?)
|
||||
} else {
|
||||
Err(anyhow!("Invalid cipher string"))
|
||||
}
|
||||
}
|
||||
|
||||
fn random_challenge() -> [u8; 16] {
|
||||
let mut challenge = [0u8; 16];
|
||||
rand::thread_rng().fill_bytes(&mut challenge);
|
||||
challenge
|
||||
}
|
||||
|
||||
/// Searches for a window that looks like a security prompt and set it as focused.
|
||||
///
|
||||
/// Gives up after 1.5 seconds with a delay of 500ms between each try.
|
||||
fn focus_security_prompt() -> Result<()> {
|
||||
unsafe fn try_find_and_set_focus(
|
||||
class_name: windows::core::PCSTR,
|
||||
) -> retry::OperationResult<(), ()> {
|
||||
let hwnd = unsafe { FindWindowA(class_name, None) };
|
||||
if hwnd.0 != 0 {
|
||||
set_focus(hwnd);
|
||||
return retry::OperationResult::Ok(());
|
||||
}
|
||||
retry::OperationResult::Retry(())
|
||||
}
|
||||
|
||||
let class_name = s!("Credential Dialog Xaml Host");
|
||||
retry::retry_with_index(Fixed::from_millis(500), |current_try| {
|
||||
if current_try > 3 {
|
||||
return retry::OperationResult::Err(());
|
||||
}
|
||||
|
||||
unsafe { try_find_and_set_focus(class_name) }
|
||||
})
|
||||
.map_err(|_| anyhow!("Failed to find security prompt"))
|
||||
}
|
||||
|
||||
fn set_focus(window: HWND) {
|
||||
let mut pressed = false;
|
||||
|
||||
unsafe {
|
||||
// Simulate holding down Alt key to bypass windows limitations
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getasynckeystate#return-value
|
||||
// The most significant bit indicates if the key is currently being pressed. This means the
|
||||
// value will be negative if the key is pressed.
|
||||
if GetAsyncKeyState(VK_MENU.0 as i32) >= 0 {
|
||||
pressed = true;
|
||||
keybd_event(VK_MENU.0 as u8, 0, KEYEVENTF_EXTENDEDKEY, 0);
|
||||
}
|
||||
SetForegroundWindow(window);
|
||||
SetFocus(window);
|
||||
if pressed {
|
||||
keybd_event(
|
||||
VK_MENU.0 as u8,
|
||||
0,
|
||||
KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP,
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyMaterial {
|
||||
fn digest_material(&self) -> String {
|
||||
match self.client_key_part_b64.as_deref() {
|
||||
Some(client_key_part_b64) => {
|
||||
format!("{}|{}", self.os_key_part_b64, client_key_part_b64)
|
||||
}
|
||||
None => self.os_key_part_b64.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn derive_key(&self) -> Result<GenericArray<u8, typenum::U32>> {
|
||||
Ok(Sha256::digest(self.digest_material()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::biometric::BiometricTrait;
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "manual_test")]
|
||||
fn test_derive_key_material() {
|
||||
let iv_input = "l9fhDUP/wDJcKwmEzcb/3w==";
|
||||
let result = <Biometric as BiometricTrait>::derive_key_material(Some(iv_input)).unwrap();
|
||||
let key = base64_engine.decode(result.key_b64).unwrap();
|
||||
assert_eq!(key.len(), 32);
|
||||
assert_eq!(result.iv_b64, iv_input)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "manual_test")]
|
||||
fn test_derive_key_material_no_iv() {
|
||||
let result = <Biometric as BiometricTrait>::derive_key_material(None).unwrap();
|
||||
let key = base64_engine.decode(result.key_b64).unwrap();
|
||||
assert_eq!(key.len(), 32);
|
||||
let iv = base64_engine.decode(result.iv_b64).unwrap();
|
||||
assert_eq!(iv.len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "manual_test")]
|
||||
fn test_prompt() {
|
||||
<Biometric as BiometricTrait>::prompt(
|
||||
vec![0, 0, 0, 0, 0, 0, 0, 0],
|
||||
String::from("Hello from Rust"),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "manual_test")]
|
||||
fn test_available() {
|
||||
assert!(<Biometric as BiometricTrait>::available().unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt() {
|
||||
let key_material = KeyMaterial {
|
||||
os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(),
|
||||
client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()),
|
||||
};
|
||||
let iv_b64 = "l9fhDUP/wDJcKwmEzcb/3w==".to_owned();
|
||||
let secret = encrypt("secret", &key_material, &iv_b64)
|
||||
.unwrap()
|
||||
.parse::<CipherString>()
|
||||
.unwrap();
|
||||
|
||||
match secret {
|
||||
CipherString::AesCbc256_B64 { iv, data: _ } => {
|
||||
assert_eq!(iv_b64, base64_engine.encode(&iv));
|
||||
}
|
||||
_ => panic!("Invalid cipher string"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt() {
|
||||
let secret =
|
||||
CipherString::from_str("0.l9fhDUP/wDJcKwmEzcb/3w==|uP4LcqoCCj5FxBDP77NV6Q==").unwrap(); // output from test_encrypt
|
||||
let key_material = KeyMaterial {
|
||||
os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(),
|
||||
client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()),
|
||||
};
|
||||
assert_eq!(decrypt(&secret, &key_material).unwrap(), "secret")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_biometric_secret_requires_key() {
|
||||
let result = <Biometric as BiometricTrait>::get_biometric_secret("", "", None);
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
"Key material is required for Windows Hello protected keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_biometric_secret_handles_unencrypted_secret() {
|
||||
scopeguard::defer! {
|
||||
crate::password::delete_password("test", "test").unwrap();
|
||||
}
|
||||
let test = "test";
|
||||
let secret = "password";
|
||||
let key_material = KeyMaterial {
|
||||
os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(),
|
||||
client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()),
|
||||
};
|
||||
crate::password::set_password(test, test, secret).unwrap();
|
||||
let result =
|
||||
<Biometric as BiometricTrait>::get_biometric_secret(test, test, Some(key_material))
|
||||
.unwrap();
|
||||
assert_eq!(result, secret);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_biometric_secret_handles_encrypted_secret() {
|
||||
scopeguard::defer! {
|
||||
crate::password::delete_password("test", "test").unwrap();
|
||||
}
|
||||
let test = "test";
|
||||
let secret =
|
||||
CipherString::from_str("0.l9fhDUP/wDJcKwmEzcb/3w==|uP4LcqoCCj5FxBDP77NV6Q==").unwrap(); // output from test_encrypt
|
||||
let key_material = KeyMaterial {
|
||||
os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(),
|
||||
client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()),
|
||||
};
|
||||
crate::password::set_password(test, test, &secret.to_string()).unwrap();
|
||||
|
||||
let result =
|
||||
<Biometric as BiometricTrait>::get_biometric_secret(test, test, Some(key_material))
|
||||
.unwrap();
|
||||
assert_eq!(result, "secret");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_biometric_secret_requires_key() {
|
||||
let result = <Biometric as BiometricTrait>::set_biometric_secret("", "", "", None, "");
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err().to_string(),
|
||||
"Key material is required for Windows Hello protected keys"
|
||||
);
|
||||
}
|
||||
|
||||
fn key_material() -> KeyMaterial {
|
||||
KeyMaterial {
|
||||
os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(),
|
||||
client_key_part_b64: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_material_produces_valid_key() {
|
||||
let result = key_material().derive_key().unwrap();
|
||||
assert_eq!(result.len(), 32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_material_uses_os_part() {
|
||||
let mut key_material = key_material();
|
||||
let result = key_material.derive_key().unwrap();
|
||||
key_material.os_key_part_b64 = "BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned();
|
||||
let result2 = key_material.derive_key().unwrap();
|
||||
assert_ne!(result, result2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_material_uses_client_part() {
|
||||
let mut key_material = key_material();
|
||||
let result = key_material.derive_key().unwrap();
|
||||
key_material.client_key_part_b64 =
|
||||
Some("BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned());
|
||||
let result2 = key_material.derive_key().unwrap();
|
||||
assert_ne!(result, result2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_material_produces_consistent_os_only_key() {
|
||||
let mut key_material = key_material();
|
||||
key_material.client_key_part_b64 = None;
|
||||
let result = key_material.derive_key().unwrap();
|
||||
assert_eq!(
|
||||
result,
|
||||
[
|
||||
81, 100, 62, 172, 151, 119, 182, 58, 123, 38, 129, 116, 209, 253, 66, 118, 218,
|
||||
237, 236, 155, 201, 234, 11, 198, 229, 171, 246, 144, 71, 188, 84, 246
|
||||
]
|
||||
.into()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_material_produces_unique_os_only_key() {
|
||||
let mut key_material = key_material();
|
||||
key_material.client_key_part_b64 = None;
|
||||
let result = key_material.derive_key().unwrap();
|
||||
key_material.os_key_part_b64 = "BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned();
|
||||
let result2 = key_material.derive_key().unwrap();
|
||||
assert_ne!(result, result2);
|
||||
}
|
||||
}
|
||||
56
apps/desktop/desktop_native/core/src/clipboard.rs
Normal file
56
apps/desktop/desktop_native/core/src/clipboard.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use anyhow::Result;
|
||||
use arboard::{Clipboard, Set};
|
||||
|
||||
pub fn read() -> Result<String> {
|
||||
let mut clipboard = Clipboard::new()?;
|
||||
|
||||
Ok(clipboard.get_text()?)
|
||||
}
|
||||
|
||||
pub fn write(text: &str, password: bool) -> Result<()> {
|
||||
let mut clipboard = Clipboard::new()?;
|
||||
|
||||
let set = clipboard_set(clipboard.set(), password);
|
||||
|
||||
set.text(text)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Exclude from windows clipboard history
|
||||
#[cfg(target_os = "windows")]
|
||||
fn clipboard_set(set: Set, password: bool) -> Set {
|
||||
use arboard::SetExtWindows;
|
||||
|
||||
if password {
|
||||
set.exclude_from_cloud().exclude_from_history()
|
||||
} else {
|
||||
set
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for clipboard to be available on linux
|
||||
#[cfg(target_os = "linux")]
|
||||
fn clipboard_set(set: Set, _password: bool) -> Set {
|
||||
use arboard::SetExtLinux;
|
||||
|
||||
set.wait()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn clipboard_set(set: Set, _password: bool) -> Set {
|
||||
set
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
#[cfg(any(feature = "manual_test", not(target_os = "linux")))]
|
||||
fn test_write_read() {
|
||||
let message = "Hello world!";
|
||||
|
||||
write(message, false).unwrap();
|
||||
assert_eq!(message, read().unwrap());
|
||||
}
|
||||
}
|
||||
212
apps/desktop/desktop_native/core/src/crypto/cipher_string.rs
Normal file
212
apps/desktop/desktop_native/core/src/crypto/cipher_string.rs
Normal file
@@ -0,0 +1,212 @@
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
|
||||
|
||||
use crate::error::{CSParseError, Error};
|
||||
|
||||
#[allow(unused, non_camel_case_types)]
|
||||
pub enum CipherString {
|
||||
// 0
|
||||
AesCbc256_B64 {
|
||||
iv: [u8; 16],
|
||||
data: Vec<u8>,
|
||||
},
|
||||
// 1
|
||||
AesCbc128_HmacSha256_B64 {
|
||||
iv: [u8; 16],
|
||||
mac: [u8; 32],
|
||||
data: Vec<u8>,
|
||||
},
|
||||
// 2
|
||||
AesCbc256_HmacSha256_B64 {
|
||||
iv: [u8; 16],
|
||||
mac: [u8; 32],
|
||||
data: Vec<u8>,
|
||||
},
|
||||
// 3
|
||||
Rsa2048_OaepSha256_B64 {
|
||||
data: Vec<u8>,
|
||||
},
|
||||
// 4
|
||||
Rsa2048_OaepSha1_B64 {
|
||||
data: Vec<u8>,
|
||||
},
|
||||
// 5
|
||||
Rsa2048_OaepSha256_HmacSha256_B64 {
|
||||
mac: [u8; 32],
|
||||
data: Vec<u8>,
|
||||
},
|
||||
// 6
|
||||
Rsa2048_OaepSha1_HmacSha256_B64 {
|
||||
mac: [u8; 32],
|
||||
data: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
// We manually implement these to make sure we don't print any sensitive data
|
||||
impl std::fmt::Debug for CipherString {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("CipherString")
|
||||
.field("type", &self.enc_type_name())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_len_error(expected: usize) -> impl Fn(Vec<u8>) -> CSParseError {
|
||||
move |e: Vec<_>| CSParseError::InvalidBase64Length {
|
||||
expected,
|
||||
got: e.len(),
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CipherString {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (enc_type, data) = s.split_once('.').ok_or(CSParseError::NoType)?;
|
||||
|
||||
let parts: Vec<_> = data.split('|').collect();
|
||||
match (enc_type, parts.len()) {
|
||||
("0", 2) => {
|
||||
let iv_str = parts[0];
|
||||
let data_str = parts[1];
|
||||
|
||||
let iv = base64_engine
|
||||
.decode(iv_str)
|
||||
.map_err(CSParseError::InvalidBase64)?
|
||||
.try_into()
|
||||
.map_err(invalid_len_error(16))?;
|
||||
|
||||
let data = base64_engine
|
||||
.decode(data_str)
|
||||
.map_err(CSParseError::InvalidBase64)?;
|
||||
|
||||
Ok(CipherString::AesCbc256_B64 { iv, data })
|
||||
}
|
||||
|
||||
("1" | "2", 3) => {
|
||||
let iv_str = parts[0];
|
||||
let data_str = parts[1];
|
||||
let mac_str = parts[2];
|
||||
|
||||
let iv = base64_engine
|
||||
.decode(iv_str)
|
||||
.map_err(CSParseError::InvalidBase64)?
|
||||
.try_into()
|
||||
.map_err(invalid_len_error(16))?;
|
||||
|
||||
let mac = base64_engine
|
||||
.decode(mac_str)
|
||||
.map_err(CSParseError::InvalidBase64)?
|
||||
.try_into()
|
||||
.map_err(invalid_len_error(32))?;
|
||||
|
||||
let data = base64_engine
|
||||
.decode(data_str)
|
||||
.map_err(CSParseError::InvalidBase64)?;
|
||||
|
||||
if enc_type == "1" {
|
||||
Ok(CipherString::AesCbc128_HmacSha256_B64 { iv, mac, data })
|
||||
} else {
|
||||
Ok(CipherString::AesCbc256_HmacSha256_B64 { iv, mac, data })
|
||||
}
|
||||
}
|
||||
|
||||
("3" | "4", 1) => {
|
||||
let data = base64_engine
|
||||
.decode(data)
|
||||
.map_err(CSParseError::InvalidBase64)?;
|
||||
if enc_type == "3" {
|
||||
Ok(CipherString::Rsa2048_OaepSha256_B64 { data })
|
||||
} else {
|
||||
Ok(CipherString::Rsa2048_OaepSha1_B64 { data })
|
||||
}
|
||||
}
|
||||
("5" | "6", 2) => {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
(enc_type, parts) => Err(CSParseError::InvalidType {
|
||||
enc_type: enc_type.to_string(),
|
||||
parts,
|
||||
}
|
||||
.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CipherString {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}.", self.enc_type())?;
|
||||
|
||||
let mut parts = Vec::<&[u8]>::new();
|
||||
|
||||
match self {
|
||||
CipherString::AesCbc256_B64 { iv, data } => {
|
||||
parts.push(iv);
|
||||
parts.push(data);
|
||||
}
|
||||
CipherString::AesCbc128_HmacSha256_B64 { iv, mac, data } => {
|
||||
parts.push(iv);
|
||||
parts.push(data);
|
||||
parts.push(mac);
|
||||
}
|
||||
CipherString::AesCbc256_HmacSha256_B64 { iv, mac, data } => {
|
||||
parts.push(iv);
|
||||
parts.push(data);
|
||||
parts.push(mac);
|
||||
}
|
||||
CipherString::Rsa2048_OaepSha256_B64 { data } => {
|
||||
parts.push(data);
|
||||
}
|
||||
CipherString::Rsa2048_OaepSha1_B64 { data } => {
|
||||
parts.push(data);
|
||||
}
|
||||
CipherString::Rsa2048_OaepSha256_HmacSha256_B64 { mac, data } => {
|
||||
parts.push(data);
|
||||
parts.push(mac);
|
||||
}
|
||||
CipherString::Rsa2048_OaepSha1_HmacSha256_B64 { mac, data } => {
|
||||
parts.push(data);
|
||||
parts.push(mac);
|
||||
}
|
||||
}
|
||||
|
||||
for i in 0..parts.len() {
|
||||
if i == parts.len() - 1 {
|
||||
write!(f, "{}", base64_engine.encode(parts[i]))?;
|
||||
} else {
|
||||
write!(f, "{}|", base64_engine.encode(parts[i]))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CipherString {
|
||||
fn enc_type(&self) -> u8 {
|
||||
match self {
|
||||
CipherString::AesCbc256_B64 { .. } => 0,
|
||||
CipherString::AesCbc128_HmacSha256_B64 { .. } => 1,
|
||||
CipherString::AesCbc256_HmacSha256_B64 { .. } => 2,
|
||||
CipherString::Rsa2048_OaepSha256_B64 { .. } => 3,
|
||||
CipherString::Rsa2048_OaepSha1_B64 { .. } => 4,
|
||||
CipherString::Rsa2048_OaepSha256_HmacSha256_B64 { .. } => 5,
|
||||
CipherString::Rsa2048_OaepSha1_HmacSha256_B64 { .. } => 6,
|
||||
}
|
||||
}
|
||||
|
||||
fn enc_type_name(&self) -> &str {
|
||||
match self.enc_type() {
|
||||
0 => "AesCbc256_B64",
|
||||
1 => "AesCbc128_HmacSha256_B64",
|
||||
2 => "AesCbc256_HmacSha256_B64",
|
||||
3 => "Rsa2048_OaepSha256_B64",
|
||||
4 => "Rsa2048_OaepSha1_B64",
|
||||
5 => "Rsa2048_OaepSha256_HmacSha256_B64",
|
||||
6 => "Rsa2048_OaepSha1_HmacSha256_B64",
|
||||
_ => "Unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
39
apps/desktop/desktop_native/core/src/crypto/crypto.rs
Normal file
39
apps/desktop/desktop_native/core/src/crypto/crypto.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
//! Cryptographic primitives used in the SDK
|
||||
|
||||
use aes::cipher::{
|
||||
block_padding::Pkcs7, generic_array::GenericArray, typenum::U32, BlockDecryptMut,
|
||||
BlockEncryptMut, KeyIvInit,
|
||||
};
|
||||
|
||||
use crate::error::{CryptoError, Result};
|
||||
|
||||
use super::CipherString;
|
||||
|
||||
pub fn decrypt_aes256(
|
||||
iv: &[u8; 16],
|
||||
data: &Vec<u8>,
|
||||
key: GenericArray<u8, U32>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let iv = GenericArray::from_slice(iv);
|
||||
let mut data = data.clone();
|
||||
let decrypted_key_slice = cbc::Decryptor::<aes::Aes256>::new(&key, iv)
|
||||
.decrypt_padded_mut::<Pkcs7>(&mut data)
|
||||
.map_err(|_| CryptoError::KeyDecrypt)?;
|
||||
|
||||
// Data is decrypted in place and returns a subslice of the original Vec, to avoid cloning it, we truncate to the subslice length
|
||||
let decrypted_len = decrypted_key_slice.len();
|
||||
data.truncate(decrypted_len);
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub fn encrypt_aes256(
|
||||
data_dec: &[u8],
|
||||
iv: [u8; 16],
|
||||
key: GenericArray<u8, U32>,
|
||||
) -> Result<CipherString> {
|
||||
let data = cbc::Encryptor::<aes::Aes256>::new(&key, &iv.into())
|
||||
.encrypt_padded_vec_mut::<Pkcs7>(data_dec);
|
||||
|
||||
Ok(CipherString::AesCbc256_B64 { iv, data })
|
||||
}
|
||||
5
apps/desktop/desktop_native/core/src/crypto/mod.rs
Normal file
5
apps/desktop/desktop_native/core/src/crypto/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub use cipher_string::*;
|
||||
pub use crypto::*;
|
||||
|
||||
mod cipher_string;
|
||||
mod crypto;
|
||||
43
apps/desktop/desktop_native/core/src/error.rs
Normal file
43
apps/desktop/desktop_native/core/src/error.rs
Normal file
@@ -0,0 +1,43 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("Error parsing CipherString: {0}")]
|
||||
InvalidCipherString(#[from] CSParseError),
|
||||
|
||||
#[error("Cryptography Error, {0}")]
|
||||
Crypto(#[from] CryptoError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CSParseError {
|
||||
#[error("No type detected, missing '.' separator")]
|
||||
NoType,
|
||||
#[error("Invalid type, got {enc_type} with {parts} parts")]
|
||||
InvalidType { enc_type: String, parts: usize },
|
||||
#[error("Error decoding base64: {0}")]
|
||||
InvalidBase64(#[from] base64::DecodeError),
|
||||
#[error("Invalid base64 length: expected {expected}, got {got}")]
|
||||
InvalidBase64Length { expected: usize, got: usize },
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum CryptoError {
|
||||
#[error("Error while decrypting cipher string")]
|
||||
KeyDecrypt,
|
||||
}
|
||||
|
||||
// Ensure that the error messages implement Send and Sync
|
||||
#[cfg(test)]
|
||||
const _: () = {
|
||||
fn assert_send<T: Send>() {}
|
||||
fn assert_sync<T: Sync>() {}
|
||||
fn assert_all() {
|
||||
assert_send::<Error>();
|
||||
assert_sync::<Error>();
|
||||
}
|
||||
};
|
||||
|
||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
5
apps/desktop/desktop_native/core/src/lib.rs
Normal file
5
apps/desktop/desktop_native/core/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod biometric;
|
||||
pub mod clipboard;
|
||||
pub mod crypto;
|
||||
pub mod error;
|
||||
pub mod password;
|
||||
59
apps/desktop/desktop_native/core/src/password/macos.rs
Normal file
59
apps/desktop/desktop_native/core/src/password/macos.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use anyhow::Result;
|
||||
use security_framework::passwords::{
|
||||
delete_generic_password, get_generic_password, set_generic_password,
|
||||
};
|
||||
|
||||
pub fn get_password(service: &str, account: &str) -> Result<String> {
|
||||
let result = String::from_utf8(get_generic_password(&service, &account)?)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn get_password_keytar(service: &str, account: &str) -> Result<String> {
|
||||
get_password(service, account)
|
||||
}
|
||||
|
||||
pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> {
|
||||
let result = set_generic_password(&service, &account, password.as_bytes())?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn delete_password(service: &str, account: &str) -> Result<()> {
|
||||
let result = delete_generic_password(&service, &account)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[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!(
|
||||
"The specified item could not be found in the keychain.",
|
||||
e.to_string()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_no_password() {
|
||||
match get_password("Unknown", "Unknown") {
|
||||
Ok(_) => panic!("Got a result"),
|
||||
Err(e) => assert_eq!(
|
||||
"The specified item could not be found in the keychain.",
|
||||
e.to_string()
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
5
apps/desktop/desktop_native/core/src/password/mod.rs
Normal file
5
apps/desktop/desktop_native/core/src/password/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[cfg_attr(target_os = "linux", path = "unix.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
mod password;
|
||||
pub use password::*;
|
||||
91
apps/desktop/desktop_native/core/src/password/unix.rs
Normal file
91
apps/desktop/desktop_native/core/src/password/unix.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use libsecret::{password_clear_sync, password_lookup_sync, password_store_sync, Schema};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn get_password(service: &str, account: &str) -> Result<String> {
|
||||
let res = password_lookup_sync(
|
||||
Some(&get_schema()),
|
||||
build_attributes(service, account),
|
||||
gio::Cancellable::NONE,
|
||||
)?;
|
||||
|
||||
match res {
|
||||
Some(s) => Ok(String::from(s)),
|
||||
None => Err(anyhow!("No password found")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_password_keytar(service: &str, account: &str) -> Result<String> {
|
||||
get_password(service, account)
|
||||
}
|
||||
|
||||
pub fn set_password(service: &str, account: &str, password: &str) -> Result<()> {
|
||||
let result = password_store_sync(
|
||||
Some(&get_schema()),
|
||||
build_attributes(service, account),
|
||||
Some(&libsecret::COLLECTION_DEFAULT),
|
||||
&format!("{}/{}", service, account),
|
||||
password,
|
||||
gio::Cancellable::NONE,
|
||||
)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn delete_password(service: &str, account: &str) -> Result<()> {
|
||||
let result = password_clear_sync(
|
||||
Some(&get_schema()),
|
||||
build_attributes(service, account),
|
||||
gio::Cancellable::NONE,
|
||||
)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn get_schema() -> Schema {
|
||||
let mut attributes = std::collections::HashMap::new();
|
||||
attributes.insert("service", libsecret::SchemaAttributeType::String);
|
||||
attributes.insert("account", libsecret::SchemaAttributeType::String);
|
||||
|
||||
libsecret::Schema::new(
|
||||
"org.freedesktop.Secret.Generic",
|
||||
libsecret::SchemaFlags::NONE,
|
||||
attributes,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_attributes<'a>(service: &'a str, account: &'a str) -> HashMap<&'a str, &'a str> {
|
||||
let mut attributes = HashMap::new();
|
||||
attributes.insert("service", service);
|
||||
attributes.insert("account", account);
|
||||
|
||||
attributes
|
||||
}
|
||||
|
||||
#[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!("No password found", e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_no_password() {
|
||||
match get_password("BitwardenTest", "BitwardenTest") {
|
||||
Ok(_) => panic!("Got a result"),
|
||||
Err(e) => assert_eq!("No password found", e.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
175
apps/desktop/desktop_native/core/src/password/windows.rs
Normal file
175
apps/desktop/desktop_native/core/src/password/windows.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use widestring::{U16CString, U16String};
|
||||
use windows::{
|
||||
core::{PCWSTR, PWSTR},
|
||||
Win32::{
|
||||
Foundation::{ERROR_NOT_FOUND, FILETIME},
|
||||
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(
|
||||
PCWSTR(target_name.as_ptr()),
|
||||
CRED_TYPE_GENERIC,
|
||||
CRED_FLAGS_NONE,
|
||||
credential_ptr,
|
||||
)
|
||||
};
|
||||
|
||||
scopeguard::defer!({
|
||||
unsafe { CredFree(credential as *mut _) };
|
||||
});
|
||||
|
||||
result.map_err(|e| anyhow!(convert_error(e)))?;
|
||||
|
||||
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(
|
||||
PCWSTR(target_name.as_ptr()),
|
||||
CRED_TYPE_GENERIC,
|
||||
CRED_FLAGS_NONE,
|
||||
credential_ptr,
|
||||
)
|
||||
};
|
||||
|
||||
scopeguard::defer!({
|
||||
unsafe { CredFree(credential as *mut _) };
|
||||
});
|
||||
|
||||
result?;
|
||||
|
||||
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 mut target_name = U16CString::from_str(target_name(service, account))?;
|
||||
let mut 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_mut_ptr()),
|
||||
Comment: PWSTR::null(),
|
||||
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::null(),
|
||||
UserName: PWSTR(user_name.as_mut_ptr()),
|
||||
};
|
||||
|
||||
unsafe { CredWriteW(&credential, 0) }?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_password(service: &str, account: &str) -> Result<()> {
|
||||
let target_name = U16CString::from_str(target_name(service, account))?;
|
||||
|
||||
unsafe {
|
||||
CredDeleteW(
|
||||
PCWSTR(target_name.as_ptr()),
|
||||
CRED_TYPE_GENERIC,
|
||||
CRED_FLAGS_NONE,
|
||||
)?
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn target_name(service: &str, account: &str) -> String {
|
||||
format!("{}/{}", service, account)
|
||||
}
|
||||
|
||||
// Convert the internal WIN32 errors to descriptive messages
|
||||
fn convert_error(e: windows::core::Error) -> String {
|
||||
if e == ERROR_NOT_FOUND.into() {
|
||||
return "Password not found.".to_string();
|
||||
}
|
||||
e.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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user