1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 01:33:33 +00:00

Rework Desktop Biometrics (#5234)

This commit is contained in:
Matt Gibson
2023-04-18 09:09:47 -04:00
committed by GitHub
parent 4852992662
commit 830af7b06d
55 changed files with 2497 additions and 564 deletions

View File

@@ -1,9 +1,38 @@
use anyhow::{bail, Result};
pub fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
bail!("platform not supported");
}
use crate::biometrics::{KeyMaterial, OsDerivedKey};
pub fn available() -> Result<bool> {
bail!("platform not supported");
/// 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");
}
}

View File

@@ -1,5 +1,28 @@
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::*;
pub use biometric::Biometric;
use crate::biometrics::{KeyMaterial, OsDerivedKey};
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>;
}

View File

@@ -1,9 +1,38 @@
use anyhow::{bail, Result};
pub fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
bail!("platform not supported");
}
use crate::biometrics::{KeyMaterial, OsDerivedKey};
pub fn available() -> Result<bool> {
bail!("platform not supported");
/// 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");
}
}

View File

@@ -1,8 +1,21 @@
use anyhow::Result;
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::{
h,
core::{factory, HSTRING},
Foundation::IAsyncOperation,
Security::Credentials::UI::*,
Security::{
Credentials::{
KeyCredentialCreationOption, KeyCredentialManager, KeyCredentialStatus, UI::*,
},
Cryptography::CryptographicBuffer,
},
Win32::{
Foundation::HWND,
System::WinRT::IUserConsentVerifierInterop,
@@ -11,40 +24,195 @@ use windows::{
keybd_event, GetAsyncKeyState, SetFocus, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP,
VK_MENU,
},
WindowsAndMessaging::SetForegroundWindow,
WindowsAndMessaging::{FindWindowA, SetForegroundWindow},
},
},
};
pub fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool> {
let h = isize::from_le_bytes(hwnd.clone().try_into().unwrap());
let window = HWND(h);
use crate::{
biometrics::{KeyMaterial, OsDerivedKey},
crypto::{self, CipherString},
};
// 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);
/// The Windows OS implementation of the biometric trait.
pub struct Biometric {}
let interop = factory::<UserConsentVerifier, IUserConsentVerifierInterop>()?;
let operation: IAsyncOperation<UserConsentVerificationResult> =
unsafe { interop.RequestVerificationForWindowAsync(window, &HSTRING::from(message))? };
let result = operation.get()?;
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);
match result {
UserConsentVerificationResult::Verified => Ok(true),
_ => Ok(false),
// 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);
}
}
}
}
pub fn available() -> Result<bool> {
let ucv_available = UserConsentVerifier::CheckAvailabilityAsync()?.get()?;
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()))?;
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),
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 = windows::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;
@@ -70,14 +238,49 @@ fn set_focus(window: HWND) {
}
}
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() {
prompt(
<Biometric as BiometricTrait>::prompt(
vec![0, 0, 0, 0, 0, 0, 0, 0],
String::from("Hello from Rust"),
)
@@ -87,6 +290,145 @@ mod tests {
#[test]
#[cfg(feature = "manual_test")]
fn test_available() {
assert!(available().unwrap())
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);
}
}

View 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",
}
}
}

View 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 })
}

View File

@@ -0,0 +1,5 @@
pub use cipher_string::*;
pub use crypto::*;
mod cipher_string;
mod crypto;

View 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>;

View File

@@ -2,6 +2,8 @@
extern crate napi_derive;
mod biometric;
mod crypto;
mod error;
mod password;
#[napi]
@@ -41,18 +43,67 @@ pub mod passwords {
#[napi]
pub mod biometrics {
use super::biometric::{Biometric, BiometricTrait};
// Prompt for biometric confirmation
#[napi]
pub async fn prompt(
hwnd: napi::bindgen_prelude::Buffer,
message: String,
) -> napi::Result<bool> {
super::biometric::prompt(hwnd.into(), message)
.map_err(|e| napi::Error::from_reason(e.to_string()))
Biometric::prompt(hwnd.into(), message).map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn available() -> napi::Result<bool> {
super::biometric::available().map_err(|e| napi::Error::from_reason(e.to_string()))
Biometric::available().map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn set_biometric_secret(
service: String,
account: String,
secret: String,
key_material: Option<KeyMaterial>,
iv_b64: String,
) -> napi::Result<String> {
Biometric::set_biometric_secret(&service, &account, &secret, key_material, &iv_b64)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn get_biometric_secret(
service: String,
account: String,
key_material: Option<KeyMaterial>,
) -> napi::Result<String> {
let result = Biometric::get_biometric_secret(&service, &account, key_material)
.map_err(|e| napi::Error::from_reason(e.to_string()));
result
}
/// Derives key material from biometric data. Returns a string encoded with a
/// base64 encoded key and the base64 encoded challenge used to create it
/// separated by a `|` character.
///
/// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will be generated.
///
/// `format!("<key_base64>|<iv_base64>")`
#[napi]
pub async fn derive_key_material(iv: Option<String>) -> napi::Result<OsDerivedKey> {
Biometric::derive_key_material(iv.as_deref())
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi(object)]
pub struct KeyMaterial {
pub os_key_part_b64: String,
pub client_key_part_b64: Option<String>,
}
#[napi(object)]
pub struct OsDerivedKey {
pub key_b64: String,
pub iv_b64: String,
}
}