mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 21:50:15 +00:00
Merge branch 'main' into passkey-window-working
This commit is contained in:
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ function log(configObj) {
|
||||
|
||||
function loadConfig(configName) {
|
||||
try {
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
return require(`./${configName}.json`);
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.code === "MODULE_NOT_FOUND") {
|
||||
|
||||
1
apps/desktop/desktop_native/.gitignore
vendored
1
apps/desktop/desktop_native/.gitignore
vendored
@@ -5,3 +5,4 @@ index.node
|
||||
npm-debug.log*
|
||||
*.node
|
||||
dist
|
||||
windows_pluginauthenticator_bindings.rs
|
||||
|
||||
625
apps/desktop/desktop_native/Cargo.lock
generated
625
apps/desktop/desktop_native/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,19 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["napi", "core", "proxy", "macos_provider"]
|
||||
members = ["napi", "core", "proxy", "macos_provider", "windows-plugin-authenticator"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.0"
|
||||
license = "GPL-3.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = "=1.0.94"
|
||||
log = "=0.4.25"
|
||||
serde = "=1.0.209"
|
||||
serde_json = "=1.0.127"
|
||||
tokio = "=1.43.0"
|
||||
tokio-util = "=0.7.13"
|
||||
tokio-stream = "=0.1.15"
|
||||
thiserror = "=1.0.69"
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
name = "desktop_core"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = ["sys"]
|
||||
manual_test = []
|
||||
|
||||
sys = [
|
||||
default = [
|
||||
"dep:widestring",
|
||||
"dep:windows",
|
||||
"dep:core-foundation",
|
||||
@@ -18,62 +15,60 @@ sys = [
|
||||
"dep:zbus",
|
||||
"dep:zbus_polkit",
|
||||
]
|
||||
manual_test = []
|
||||
|
||||
[dependencies]
|
||||
aes = "=0.8.4"
|
||||
anyhow = "=1.0.94"
|
||||
anyhow = { workspace = true }
|
||||
arboard = { version = "=3.4.1", default-features = false, features = [
|
||||
"wayland-data-control",
|
||||
"wayland-data-control",
|
||||
] }
|
||||
argon2 = { version = "=0.5.3", features = ["zeroize"] }
|
||||
async-stream = "=0.3.6"
|
||||
base64 = "=0.22.1"
|
||||
byteorder = "=1.5.0"
|
||||
cbc = { version = "=0.1.2", features = ["alloc"] }
|
||||
homedir = "=0.3.4"
|
||||
libc = "=0.2.162"
|
||||
pin-project = "=1.1.7"
|
||||
dirs = "=5.0.1"
|
||||
pin-project = "=1.1.8"
|
||||
dirs = "=6.0.0"
|
||||
futures = "=0.3.31"
|
||||
interprocess = { version = "=2.2.1", features = ["tokio"] }
|
||||
log = "=0.4.22"
|
||||
log = { workspace = true }
|
||||
rand = "=0.8.5"
|
||||
retry = "=2.0.0"
|
||||
russh-cryptovec = "=0.7.3"
|
||||
scopeguard = "=1.2.0"
|
||||
sha2 = "=0.10.8"
|
||||
ssh-encoding = "=0.2.0"
|
||||
ssh-key = { version = "=0.6.7", default-features = false, features = [
|
||||
"encryption",
|
||||
"ed25519",
|
||||
"rsa",
|
||||
"getrandom",
|
||||
"encryption",
|
||||
"ed25519",
|
||||
"rsa",
|
||||
"getrandom",
|
||||
] }
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "23b50e3bbe6d56ef19ab0e98e8bb1462cb6d77ae" }
|
||||
tokio = { version = "=1.41.1", features = ["io-util", "sync", "macros", "net"] }
|
||||
tokio-stream = { version = "=0.1.15", features = ["net"] }
|
||||
tokio-util = { version = "=0.7.12", features = ["codec"] }
|
||||
thiserror = "=1.0.69"
|
||||
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "3d48f140fd506412d186203238993163a8c4e536" }
|
||||
tokio = { workspace = true, features = ["io-util", "sync", "macros", "net"] }
|
||||
tokio-stream = { workspace = true, features = ["net"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
thiserror = { workspace = true }
|
||||
typenum = "=1.17.0"
|
||||
rand_chacha = "=0.3.1"
|
||||
pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] }
|
||||
rsa = "=0.9.6"
|
||||
ed25519 = { version = "=2.2.3", features = ["pkcs8"] }
|
||||
sysinfo = { version = "0.32.0", features = ["windows"] }
|
||||
bytes = "1.9.0"
|
||||
sysinfo = { version = "0.33.1", features = ["windows"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
widestring = { version = "=1.1.0", optional = true }
|
||||
windows = { version = "=0.58.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",
|
||||
"Win32_System_Pipes",
|
||||
"Foundation",
|
||||
"Security_Credentials_UI",
|
||||
"Security_Cryptography",
|
||||
"Storage_Streams",
|
||||
"Win32_Foundation",
|
||||
"Win32_Security_Credentials",
|
||||
"Win32_System_WinRT",
|
||||
"Win32_UI_Input_KeyboardAndMouse",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
"Win32_System_Pipes",
|
||||
], optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dev-dependencies]
|
||||
@@ -81,12 +76,13 @@ keytar = "=0.1.6"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = { version = "=0.10.0", optional = true }
|
||||
security-framework = { version = "=3.0.0", optional = true }
|
||||
security-framework-sys = { version = "=2.12.0", optional = true }
|
||||
security-framework = { version = "=3.1.0", optional = true }
|
||||
security-framework-sys = { version = "=2.13.0", optional = true }
|
||||
desktop_objc = { path = "../objc" }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
oo7 = "=0.3.3"
|
||||
libc = "=0.2.169"
|
||||
|
||||
zbus = { version = "=4.4.0", optional = true }
|
||||
zbus_polkit = { version = "=4.0.0", optional = true }
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
#[cfg_attr(target_os = "linux", path = "unix.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn run_command(value: String) -> Result<String> {
|
||||
pub async fn run_command(_value: String) -> Result<String> {
|
||||
todo!("Unix does not support autofill");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub async fn run_command(value: String) -> Result<String> {
|
||||
pub async fn run_command(_value: String) -> Result<String> {
|
||||
todo!("Windows does not support autofill");
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
#[cfg_attr(target_os = "linux", path = "unix.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
mod biometric;
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
|
||||
pub use biometric::Biometric;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows_focus;
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::crypto::{self, CipherString};
|
||||
@@ -41,6 +46,7 @@ pub trait BiometricTrait {
|
||||
) -> Result<String>;
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result<String> {
|
||||
let iv = base64_engine
|
||||
.decode(iv_b64)?
|
||||
@@ -52,9 +58,10 @@ fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result<Str
|
||||
Ok(encrypted.to_string())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
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()?)?;
|
||||
let decrypted = crypto::decrypt_aes256(iv, data, key_material.derive_key()?)?;
|
||||
|
||||
Ok(String::from_utf8(decrypted)?)
|
||||
} else {
|
||||
|
||||
@@ -33,12 +33,10 @@ impl super::BiometricTrait for Biometric {
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(result) => {
|
||||
return Ok(result.is_authorized);
|
||||
}
|
||||
Ok(result) => Ok(result.is_authorized),
|
||||
Err(e) => {
|
||||
println!("polkit biometric error: {:?}", e);
|
||||
return Ok(false);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +50,7 @@ impl super::BiometricTrait for Biometric {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
return Ok(false);
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn derive_key_material(challenge_str: Option<&str>) -> Result<OsDerivedKey> {
|
||||
@@ -68,8 +66,8 @@ impl super::BiometricTrait for Biometric {
|
||||
// so we use a a key derived from the iv. this key is not intended to add any security
|
||||
// but only a place-holder
|
||||
let key = Sha256::digest(challenge);
|
||||
let key_b64 = base64_engine.encode(&key);
|
||||
let iv_b64 = base64_engine.encode(&challenge);
|
||||
let key_b64 = base64_engine.encode(key);
|
||||
let iv_b64 = base64_engine.encode(challenge);
|
||||
Ok(OsDerivedKey { key_b64, iv_b64 })
|
||||
}
|
||||
|
||||
@@ -100,7 +98,7 @@ impl super::BiometricTrait for Biometric {
|
||||
|
||||
let encrypted_secret = crate::password::get_password(service, account).await?;
|
||||
let secret = CipherString::from_str(&encrypted_secret)?;
|
||||
return Ok(decrypt(&secret, &key_material)?);
|
||||
decrypt(&secret, &key_material)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use std::{ffi::c_void, str::FromStr};
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
str::FromStr,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
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},
|
||||
core::{factory, h, HSTRING},
|
||||
Foundation::IAsyncOperation,
|
||||
Security::{
|
||||
Credentials::{
|
||||
@@ -14,17 +17,7 @@ use windows::{
|
||||
},
|
||||
Cryptography::CryptographicBuffer,
|
||||
},
|
||||
Win32::{
|
||||
Foundation::HWND,
|
||||
System::WinRT::IUserConsentVerifierInterop,
|
||||
UI::{
|
||||
Input::KeyboardAndMouse::{
|
||||
keybd_event, GetAsyncKeyState, SetFocus, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP,
|
||||
VK_MENU,
|
||||
},
|
||||
WindowsAndMessaging::{FindWindowA, SetForegroundWindow},
|
||||
},
|
||||
},
|
||||
Win32::{Foundation::HWND, System::WinRT::IUserConsentVerifierInterop},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -32,7 +25,10 @@ use crate::{
|
||||
crypto::CipherString,
|
||||
};
|
||||
|
||||
use super::{decrypt, encrypt};
|
||||
use super::{
|
||||
decrypt, encrypt,
|
||||
windows_focus::{focus_security_prompt, set_focus},
|
||||
};
|
||||
|
||||
/// The Windows OS implementation of the biometric trait.
|
||||
pub struct Biometric {}
|
||||
@@ -88,14 +84,14 @@ impl super::BiometricTrait for Biometric {
|
||||
let bitwarden = h!("Bitwarden");
|
||||
|
||||
let result = KeyCredentialManager::RequestCreateAsync(
|
||||
&bitwarden,
|
||||
bitwarden,
|
||||
KeyCredentialCreationOption::FailIfExists,
|
||||
)?
|
||||
.get()?;
|
||||
|
||||
let result = match result.Status()? {
|
||||
KeyCredentialStatus::CredentialAlreadyExists => {
|
||||
KeyCredentialManager::OpenAsync(&bitwarden)?.get()?
|
||||
KeyCredentialManager::OpenAsync(bitwarden)?.get()?
|
||||
}
|
||||
KeyCredentialStatus::Success => result,
|
||||
_ => return Err(anyhow!("Failed to create key credential")),
|
||||
@@ -103,8 +99,22 @@ impl super::BiometricTrait for Biometric {
|
||||
|
||||
let challenge_buffer = CryptographicBuffer::CreateFromByteArray(&challenge)?;
|
||||
let async_operation = result.Credential()?.RequestSignAsync(&challenge_buffer)?;
|
||||
focus_security_prompt()?;
|
||||
let signature = async_operation.get()?;
|
||||
focus_security_prompt();
|
||||
|
||||
let done = Arc::new(AtomicBool::new(false));
|
||||
let done_clone = done.clone();
|
||||
let _ = std::thread::spawn(move || loop {
|
||||
if !done_clone.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
focus_security_prompt();
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
let signature = async_operation.get();
|
||||
done.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let signature = signature?;
|
||||
|
||||
if signature.Status()? != KeyCredentialStatus::Success {
|
||||
return Err(anyhow!("Failed to sign data"));
|
||||
@@ -116,8 +126,8 @@ impl super::BiometricTrait for Biometric {
|
||||
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);
|
||||
let key_b64 = base64_engine.encode(key);
|
||||
let iv_b64 = base64_engine.encode(challenge);
|
||||
Ok(OsDerivedKey { key_b64, iv_b64 })
|
||||
}
|
||||
|
||||
@@ -151,12 +161,12 @@ impl super::BiometricTrait for Biometric {
|
||||
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);
|
||||
Ok(secret)
|
||||
}
|
||||
Err(_) => {
|
||||
// If the secret is not a CipherString, it is not encrypted and we can return it
|
||||
// directly.
|
||||
return Ok(encrypted_secret);
|
||||
Ok(encrypted_secret)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,57 +178,6 @@ fn random_challenge() -> [u8; 16] {
|
||||
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 let Ok(hwnd) = hwnd {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -245,20 +204,21 @@ mod tests {
|
||||
assert_eq!(iv.len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[cfg(feature = "manual_test")]
|
||||
fn test_prompt() {
|
||||
async fn test_prompt() {
|
||||
<Biometric as BiometricTrait>::prompt(
|
||||
vec![0, 0, 0, 0, 0, 0, 0, 0],
|
||||
String::from("Hello from Rust"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[tokio::test]
|
||||
#[cfg(feature = "manual_test")]
|
||||
fn test_available() {
|
||||
assert!(<Biometric as BiometricTrait>::available().unwrap())
|
||||
async fn test_available() {
|
||||
assert!(<Biometric as BiometricTrait>::available().await.unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -275,7 +235,7 @@ mod tests {
|
||||
|
||||
match secret {
|
||||
CipherString::AesCbc256_B64 { iv, data: _ } => {
|
||||
assert_eq!(iv_b64, base64_engine.encode(&iv));
|
||||
assert_eq!(iv_b64, base64_engine.encode(iv));
|
||||
}
|
||||
_ => panic!("Invalid cipher string"),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
use windows::{
|
||||
core::s,
|
||||
Win32::{
|
||||
Foundation::HWND,
|
||||
UI::{
|
||||
Input::KeyboardAndMouse::SetFocus,
|
||||
WindowsAndMessaging::{FindWindowA, SetForegroundWindow},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/// Searches for a window that looks like a security prompt and set it as focused.
|
||||
/// Only works when the process has permission to foreground, either by being in foreground
|
||||
/// Or by being given foreground permission https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setforegroundwindow#remarks
|
||||
pub fn focus_security_prompt() {
|
||||
let class_name = s!("Credential Dialog Xaml Host");
|
||||
let hwnd = unsafe { FindWindowA(class_name, None) };
|
||||
if let Ok(hwnd) = hwnd {
|
||||
set_focus(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_focus(window: HWND) {
|
||||
unsafe {
|
||||
let _ = SetForegroundWindow(window);
|
||||
let _ = SetFocus(window);
|
||||
}
|
||||
}
|
||||
@@ -9,13 +9,9 @@ use crate::error::{CryptoError, KdfParamError, Result};
|
||||
|
||||
use super::CipherString;
|
||||
|
||||
pub fn decrypt_aes256(
|
||||
iv: &[u8; 16],
|
||||
data: &Vec<u8>,
|
||||
key: GenericArray<u8, U32>,
|
||||
) -> Result<Vec<u8>> {
|
||||
pub fn decrypt_aes256(iv: &[u8; 16], data: &[u8], key: GenericArray<u8, U32>) -> Result<Vec<u8>> {
|
||||
let iv = GenericArray::from_slice(iv);
|
||||
let mut data = data.clone();
|
||||
let mut data = data.to_vec();
|
||||
let decrypted_key_slice = cbc::Decryptor::<aes::Aes256>::new(&key, iv)
|
||||
.decrypt_padded_mut::<Pkcs7>(&mut data)
|
||||
.map_err(|_| CryptoError::KeyDecrypt)?;
|
||||
@@ -54,7 +50,7 @@ pub fn argon2(
|
||||
|
||||
let mut hash = [0u8; 32];
|
||||
argon
|
||||
.hash_password_into(secret, &salt, &mut hash)
|
||||
.hash_password_into(secret, salt, &mut hash)
|
||||
.map_err(|e| KdfParamError::InvalidParams(format!("Argon2 hashing failed: {e}",)))?;
|
||||
|
||||
// Argon2 is using some stack memory that is not zeroed. Eventually some function will
|
||||
|
||||
@@ -2,4 +2,5 @@ pub use cipher_string::*;
|
||||
pub use crypto::*;
|
||||
|
||||
mod cipher_string;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod crypto;
|
||||
|
||||
@@ -44,42 +44,40 @@ pub fn path(name: &str) -> std::path::PathBuf {
|
||||
format!(r"\\.\pipe\{hash_b64}.app.{name}").into()
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "macos", not(debug_assertions)))]
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut home = dirs::home_dir().unwrap();
|
||||
|
||||
// When running in an unsandboxed environment, path is: /Users/<user>/
|
||||
// While running sandboxed, it's different: /Users/<user>/Library/Containers/com.bitwarden.desktop/Data
|
||||
//
|
||||
// We want to use App Groups in /Users/<user>/Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop,
|
||||
// so we need to remove all the components after the user.
|
||||
// Note that we subtract 3 because the root directory is counted as a component (/, Users, <user>).
|
||||
let num_components = home.components().count();
|
||||
for _ in 0..num_components - 3 {
|
||||
home.pop();
|
||||
let mut home = dirs::home_dir().unwrap();
|
||||
|
||||
// Check if the app is sandboxed by looking for the Containers directory
|
||||
let containers_position = home
|
||||
.components()
|
||||
.position(|c| c.as_os_str() == "Containers");
|
||||
|
||||
// If the app is sanboxed, we need to use the App Group directory
|
||||
if let Some(position) = containers_position {
|
||||
// We want to use App Groups in /Users/<user>/Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop,
|
||||
// so we need to remove all the components after the user. We can use the previous position to do this.
|
||||
while home.components().count() > position - 1 {
|
||||
home.pop();
|
||||
}
|
||||
|
||||
let tmp = home.join("Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop/tmp");
|
||||
|
||||
// The tmp directory might not exist, so create it
|
||||
let _ = std::fs::create_dir_all(&tmp);
|
||||
return tmp.join(format!("app.{name}"));
|
||||
}
|
||||
|
||||
let tmp = home.join("Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop/tmp");
|
||||
|
||||
// The tmp directory might not exist, so create it
|
||||
let _ = std::fs::create_dir_all(&tmp);
|
||||
tmp.join(format!("app.{name}"))
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "macos", debug_assertions))]
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
{
|
||||
// When running in debug mode, we use the tmp dir because the app is not sandboxed
|
||||
let dir = std::env::temp_dir();
|
||||
dir.join(format!("app.{name}"))
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// On Linux, we use the user's cache directory.
|
||||
// On Linux and unsandboxed Mac, we use the user's cache directory.
|
||||
let home = dirs::cache_dir().unwrap();
|
||||
let path_dir = home.join("com.bitwarden.desktop");
|
||||
|
||||
// The chache directory might not exist, so create it
|
||||
// The cache directory might not exist, so create it
|
||||
let _ = std::fs::create_dir_all(&path_dir);
|
||||
path_dir.join(format!("app.{name}"))
|
||||
}
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
pub mod autofill;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod biometric;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod clipboard;
|
||||
pub mod crypto;
|
||||
pub mod error;
|
||||
pub mod ipc;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod password;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod powermonitor;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod process_isolation;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod ssh_agent;
|
||||
|
||||
@@ -4,18 +4,18 @@ use security_framework::passwords::{
|
||||
};
|
||||
|
||||
pub async fn get_password(service: &str, account: &str) -> Result<String> {
|
||||
let result = String::from_utf8(get_generic_password(&service, &account)?)?;
|
||||
let result = String::from_utf8(get_generic_password(service, account)?)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> {
|
||||
let result = set_generic_password(&service, &account, password.as_bytes())?;
|
||||
Ok(result)
|
||||
set_generic_password(service, account, password.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_password(service: &str, account: &str) -> Result<()> {
|
||||
let result = delete_generic_password(&service, &account)?;
|
||||
Ok(result)
|
||||
delete_generic_password(service, account)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn is_available() -> Result<bool> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
#[cfg_attr(target_os = "linux", path = "unix.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
|
||||
@@ -11,9 +11,10 @@ pub async fn get_password(service: &str, account: &str) -> Result<String> {
|
||||
|
||||
async fn get_password_new(service: &str, account: &str) -> Result<String> {
|
||||
let keyring = oo7::Keyring::new().await?;
|
||||
let _ = try_prompt(&keyring).await;
|
||||
let attributes = HashMap::from([("service", service), ("account", account)]);
|
||||
let results = keyring.search_items(&attributes).await?;
|
||||
let res = results.get(0);
|
||||
let res = results.first();
|
||||
match res {
|
||||
Some(res) => {
|
||||
let secret = res.secret().await?;
|
||||
@@ -29,9 +30,10 @@ async fn get_password_legacy(service: &str, account: &str) -> Result<String> {
|
||||
let svc = dbus::Service::new().await?;
|
||||
let collection = svc.default_collection().await?;
|
||||
let keyring = oo7::Keyring::DBus(collection);
|
||||
let _ = try_prompt(&keyring).await;
|
||||
let attributes = HashMap::from([("service", service), ("account", account)]);
|
||||
let results = keyring.search_items(&attributes).await?;
|
||||
let res = results.get(0);
|
||||
let res = results.first();
|
||||
match res {
|
||||
Some(res) => {
|
||||
let secret = res.secret().await?;
|
||||
@@ -50,6 +52,7 @@ async fn get_password_legacy(service: &str, account: &str) -> Result<String> {
|
||||
|
||||
pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> {
|
||||
let keyring = oo7::Keyring::new().await?;
|
||||
let _ = try_prompt(&keyring).await;
|
||||
let attributes = HashMap::from([("service", service), ("account", account)]);
|
||||
keyring
|
||||
.create_item(
|
||||
@@ -62,13 +65,62 @@ pub async fn set_password(service: &str, account: &str, password: &str) -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a credential from the OS keyring. This function will *not* automatically
|
||||
/// prompt the user to unlock their keyring. If the keyring is locked when this
|
||||
/// is called, it will fail silently.
|
||||
pub async fn delete_password(service: &str, account: &str) -> Result<()> {
|
||||
// We need to silently fail in the event that the user's keyring was
|
||||
// locked while our application was in-use. Otherwise, when we
|
||||
// force a de-auth because we can't access keys in secure storage,
|
||||
// kwallet will notify the user that an application is "misbehaving". This
|
||||
// seems to happen because we call [delete_password] many times when a forced
|
||||
// de-auth occurs to clean up old keys.
|
||||
if is_locked().await? {
|
||||
println!("skipping deletion of old keys. OS keyring is locked.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let keyring = oo7::Keyring::new().await?;
|
||||
let attributes = HashMap::from([("service", service), ("account", account)]);
|
||||
keyring.delete(&attributes).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sends an OS notification prompt for the user to unlock/allow the application
|
||||
/// to read and write keys.
|
||||
async fn try_prompt(keyring: &oo7::Keyring) -> bool {
|
||||
keyring.unlock().await.is_ok()
|
||||
}
|
||||
|
||||
/// Keyrings on Linux cannnot be assumed to be unlocked while the user is
|
||||
/// logged in to a desktop session. Therefore, before reading or writing
|
||||
/// keys, you should check if the keyring is unlocked, and call
|
||||
/// [try_prompt] if ignoring the lock state is not an option.
|
||||
pub async fn is_locked() -> Result<bool> {
|
||||
let keyring = oo7::Keyring::new().await?;
|
||||
|
||||
// No simple way to check keyring lock state, so we just try to list items
|
||||
let items = keyring.items().await?;
|
||||
if let Some(item) = items.first() {
|
||||
return match item.is_locked().await {
|
||||
Ok(is_locked) => {
|
||||
println!("OS keyring is locked = {is_locked}");
|
||||
Ok(is_locked)
|
||||
}
|
||||
Err(_) => {
|
||||
println!("OS keyring is unlocked");
|
||||
Ok(false)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// assume it's locked
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// This will return true if a keyring is configured. However, on Linux, it does
|
||||
/// NOT indicate if the keyring is _unlocked_. Use [is_locked] to check
|
||||
/// the lock state before reading or writing keys.
|
||||
pub async fn is_available() -> Result<bool> {
|
||||
match oo7::Keyring::new().await {
|
||||
Ok(_) => Ok(true),
|
||||
|
||||
@@ -13,7 +13,7 @@ use windows::{
|
||||
|
||||
const CRED_FLAGS_NONE: u32 = 0;
|
||||
|
||||
pub async fn get_password<'a>(service: &str, account: &str) -> Result<String> {
|
||||
pub async fn get_password(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();
|
||||
@@ -42,7 +42,7 @@ pub async fn get_password<'a>(service: &str, account: &str) -> Result<String> {
|
||||
.to_string_lossy()
|
||||
};
|
||||
|
||||
Ok(String::from(password))
|
||||
Ok(password)
|
||||
}
|
||||
|
||||
pub async fn set_password(service: &str, account: &str, password: &str) -> Result<()> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "unimplemented.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "unimplemented.rs")]
|
||||
|
||||
@@ -3,5 +3,5 @@ pub async fn on_lock(_: tokio::sync::mpsc::Sender<()>) -> Result<(), Box<dyn std
|
||||
}
|
||||
|
||||
pub async fn is_lock_monitor_available() -> bool {
|
||||
return false;
|
||||
false
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#[allow(clippy::module_inception)]
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
use ssh_key::{Algorithm, HashAlg, LineEnding};
|
||||
|
||||
use super::importer::SshKey;
|
||||
|
||||
pub async fn generate_keypair(key_algorithm: String) -> Result<SshKey, anyhow::Error> {
|
||||
// sourced from cryptographically secure entropy source, with sources for all targets: https://docs.rs/getrandom
|
||||
// if it cannot be securely sourced, this will panic instead of leading to a weak key
|
||||
let mut rng: ChaCha8Rng = ChaCha8Rng::from_entropy();
|
||||
|
||||
let key = match key_algorithm.as_str() {
|
||||
"ed25519" => ssh_key::PrivateKey::random(&mut rng, Algorithm::Ed25519),
|
||||
"rsa2048" | "rsa3072" | "rsa4096" => {
|
||||
let bits = match key_algorithm.as_str() {
|
||||
"rsa2048" => 2048,
|
||||
"rsa3072" => 3072,
|
||||
"rsa4096" => 4096,
|
||||
_ => return Err(anyhow::anyhow!("Unsupported RSA key size")),
|
||||
};
|
||||
let rsa_keypair = ssh_key::private::RsaKeypair::random(&mut rng, bits)
|
||||
.or_else(|e| Err(anyhow::anyhow!(e.to_string())))?;
|
||||
|
||||
let private_key = ssh_key::PrivateKey::new(
|
||||
ssh_key::private::KeypairData::from(rsa_keypair),
|
||||
"".to_string(),
|
||||
)
|
||||
.or_else(|e| Err(anyhow::anyhow!(e.to_string())))?;
|
||||
Ok(private_key)
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!("Unsupported key algorithm"));
|
||||
}
|
||||
}
|
||||
.or_else(|e| Err(anyhow::anyhow!(e.to_string())))?;
|
||||
|
||||
let private_key_openssh = key
|
||||
.to_openssh(LineEnding::LF)
|
||||
.or_else(|e| Err(anyhow::anyhow!(e.to_string())))?;
|
||||
Ok(SshKey {
|
||||
private_key: private_key_openssh.to_string(),
|
||||
public_key: key.public_key().to_string(),
|
||||
key_fingerprint: key.fingerprint(HashAlg::Sha256).to_string(),
|
||||
})
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
use ed25519;
|
||||
use pkcs8::{
|
||||
der::Decode, EncryptedPrivateKeyInfo, ObjectIdentifier, PrivateKeyInfo, SecretDocument,
|
||||
};
|
||||
use ssh_key::{
|
||||
private::{Ed25519Keypair, Ed25519PrivateKey, RsaKeypair},
|
||||
HashAlg, LineEnding,
|
||||
};
|
||||
|
||||
const PKCS1_HEADER: &str = "-----BEGIN RSA PRIVATE KEY-----";
|
||||
const PKCS8_UNENCRYPTED_HEADER: &str = "-----BEGIN PRIVATE KEY-----";
|
||||
const PKCS8_ENCRYPTED_HEADER: &str = "-----BEGIN ENCRYPTED PRIVATE KEY-----";
|
||||
const OPENSSH_HEADER: &str = "-----BEGIN OPENSSH PRIVATE KEY-----";
|
||||
|
||||
pub const RSA_PKCS8_ALGORITHM_OID: ObjectIdentifier =
|
||||
ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
|
||||
|
||||
#[derive(Debug)]
|
||||
enum KeyType {
|
||||
Ed25519,
|
||||
Rsa,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub fn import_key(
|
||||
encoded_key: String,
|
||||
password: String,
|
||||
) -> Result<SshKeyImportResult, anyhow::Error> {
|
||||
match encoded_key.lines().next() {
|
||||
Some(PKCS1_HEADER) => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::UnsupportedKeyType,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
Some(PKCS8_UNENCRYPTED_HEADER) => {
|
||||
return match import_pkcs8_key(encoded_key, None) {
|
||||
Ok(result) => Ok(result),
|
||||
Err(_) => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
}),
|
||||
};
|
||||
}
|
||||
Some(PKCS8_ENCRYPTED_HEADER) => match import_pkcs8_key(encoded_key, Some(password)) {
|
||||
Ok(result) => {
|
||||
return Ok(result);
|
||||
}
|
||||
Err(err) => match err {
|
||||
SshKeyImportError::PasswordRequired => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::PasswordRequired,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
SshKeyImportError::WrongPassword => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::WrongPassword,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
SshKeyImportError::ParsingError => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
Some(OPENSSH_HEADER) => {
|
||||
return import_openssh_key(encoded_key, password);
|
||||
}
|
||||
Some(_) => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
None => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn import_pkcs8_key(
|
||||
encoded_key: String,
|
||||
password: Option<String>,
|
||||
) -> Result<SshKeyImportResult, SshKeyImportError> {
|
||||
let der = match SecretDocument::from_pem(&encoded_key) {
|
||||
Ok((_, doc)) => doc,
|
||||
Err(_) => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let decrypted_der = match password.clone() {
|
||||
Some(password) => {
|
||||
let encrypted_private_key_info = match EncryptedPrivateKeyInfo::from_der(der.as_bytes())
|
||||
{
|
||||
Ok(info) => info,
|
||||
Err(_) => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
match encrypted_private_key_info.decrypt(password.as_bytes()) {
|
||||
Ok(der) => der,
|
||||
Err(_) => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::WrongPassword,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
None => der,
|
||||
};
|
||||
|
||||
let key_type: KeyType = match PrivateKeyInfo::from_der(decrypted_der.as_bytes())
|
||||
.map_err(|_| SshKeyImportError::ParsingError)?
|
||||
.algorithm
|
||||
.oid
|
||||
{
|
||||
ed25519::pkcs8::ALGORITHM_OID => KeyType::Ed25519,
|
||||
RSA_PKCS8_ALGORITHM_OID => KeyType::Rsa,
|
||||
_ => KeyType::Unknown,
|
||||
};
|
||||
|
||||
match key_type {
|
||||
KeyType::Ed25519 => {
|
||||
let pk: ed25519::KeypairBytes = match password {
|
||||
Some(password) => {
|
||||
pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password)
|
||||
.map_err(|err| match err {
|
||||
ed25519::pkcs8::Error::EncryptedPrivateKey(_) => {
|
||||
SshKeyImportError::WrongPassword
|
||||
}
|
||||
_ => SshKeyImportError::ParsingError,
|
||||
})?
|
||||
}
|
||||
None => ed25519::pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key)
|
||||
.map_err(|_| SshKeyImportError::ParsingError)?,
|
||||
};
|
||||
let pk: Ed25519Keypair =
|
||||
Ed25519Keypair::from(Ed25519PrivateKey::from_bytes(&pk.secret_key));
|
||||
let private_key = ssh_key::private::PrivateKey::from(pk);
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::Success,
|
||||
ssh_key: Some(SshKey {
|
||||
private_key: private_key.to_openssh(LineEnding::LF).unwrap().to_string(),
|
||||
public_key: private_key.public_key().to_string(),
|
||||
key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
KeyType::Rsa => {
|
||||
let pk: rsa::RsaPrivateKey = match password {
|
||||
Some(password) => {
|
||||
pkcs8::DecodePrivateKey::from_pkcs8_encrypted_pem(&encoded_key, password)
|
||||
.map_err(|err| match err {
|
||||
pkcs8::Error::EncryptedPrivateKey(_) => {
|
||||
SshKeyImportError::WrongPassword
|
||||
}
|
||||
_ => SshKeyImportError::ParsingError,
|
||||
})?
|
||||
}
|
||||
None => pkcs8::DecodePrivateKey::from_pkcs8_pem(&encoded_key)
|
||||
.map_err(|_| SshKeyImportError::ParsingError)?,
|
||||
};
|
||||
let rsa_keypair: Result<RsaKeypair, ssh_key::Error> = RsaKeypair::try_from(pk);
|
||||
match rsa_keypair {
|
||||
Ok(rsa_keypair) => {
|
||||
let private_key = ssh_key::private::PrivateKey::from(rsa_keypair);
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::Success,
|
||||
ssh_key: Some(SshKey {
|
||||
private_key: private_key
|
||||
.to_openssh(LineEnding::LF)
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
public_key: private_key.public_key().to_string(),
|
||||
key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(),
|
||||
}),
|
||||
});
|
||||
}
|
||||
Err(_) => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::UnsupportedKeyType,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn import_openssh_key(
|
||||
encoded_key: String,
|
||||
password: String,
|
||||
) -> Result<SshKeyImportResult, anyhow::Error> {
|
||||
let private_key = ssh_key::private::PrivateKey::from_openssh(&encoded_key);
|
||||
let private_key = match private_key {
|
||||
Ok(k) => k,
|
||||
Err(err) => {
|
||||
match err {
|
||||
ssh_key::Error::AlgorithmUnknown
|
||||
| ssh_key::Error::AlgorithmUnsupported { algorithm: _ } => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::UnsupportedKeyType,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if private_key.is_encrypted() && password.is_empty() {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::PasswordRequired,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
let private_key = if private_key.is_encrypted() {
|
||||
match private_key.decrypt(password.as_bytes()) {
|
||||
Ok(k) => k,
|
||||
Err(_) => {
|
||||
return Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::WrongPassword,
|
||||
ssh_key: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
private_key
|
||||
};
|
||||
|
||||
match private_key.to_openssh(LineEnding::LF) {
|
||||
Ok(private_key_openssh) => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::Success,
|
||||
ssh_key: Some(SshKey {
|
||||
private_key: private_key_openssh.to_string(),
|
||||
public_key: private_key.public_key().to_string(),
|
||||
key_fingerprint: private_key.fingerprint(HashAlg::Sha256).to_string(),
|
||||
}),
|
||||
}),
|
||||
Err(_) => Ok(SshKeyImportResult {
|
||||
status: SshKeyImportStatus::ParsingError,
|
||||
ssh_key: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum SshKeyImportStatus {
|
||||
/// ssh key was parsed correctly and will be returned in the result
|
||||
Success,
|
||||
/// ssh key was parsed correctly but is encrypted and requires a password
|
||||
PasswordRequired,
|
||||
/// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect
|
||||
WrongPassword,
|
||||
/// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key
|
||||
ParsingError,
|
||||
/// ssh key type is not supported
|
||||
UnsupportedKeyType,
|
||||
}
|
||||
|
||||
pub enum SshKeyImportError {
|
||||
ParsingError,
|
||||
PasswordRequired,
|
||||
WrongPassword,
|
||||
}
|
||||
|
||||
pub struct SshKeyImportResult {
|
||||
pub status: SshKeyImportStatus,
|
||||
pub ssh_key: Option<SshKey>,
|
||||
}
|
||||
|
||||
pub struct SshKey {
|
||||
pub private_key: String,
|
||||
pub public_key: String,
|
||||
pub key_fingerprint: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn import_key_ed25519_openssh_unencrypted() {
|
||||
let private_key = include_str!("./test_keys/ed25519_openssh_unencrypted");
|
||||
let public_key = include_str!("./test_keys/ed25519_openssh_unencrypted.pub").trim();
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_ed25519_openssh_encrypted() {
|
||||
let private_key = include_str!("./test_keys/ed25519_openssh_encrypted");
|
||||
let public_key = include_str!("./test_keys/ed25519_openssh_encrypted.pub").trim();
|
||||
let result = import_key(private_key.to_string(), "password".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_rsa_openssh_unencrypted() {
|
||||
let private_key = include_str!("./test_keys/rsa_openssh_unencrypted");
|
||||
let public_key = include_str!("./test_keys/rsa_openssh_unencrypted.pub").trim();
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_rsa_openssh_encrypted() {
|
||||
let private_key = include_str!("./test_keys/rsa_openssh_encrypted");
|
||||
let public_key = include_str!("./test_keys/rsa_openssh_encrypted.pub").trim();
|
||||
let result = import_key(private_key.to_string(), "password".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_ed25519_pkcs8_unencrypted() {
|
||||
let private_key = include_str!("./test_keys/ed25519_pkcs8_unencrypted");
|
||||
let public_key =
|
||||
include_str!("./test_keys/ed25519_pkcs8_unencrypted.pub").replace("testkey", "");
|
||||
let public_key = public_key.trim();
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_rsa_pkcs8_unencrypted() {
|
||||
let private_key = include_str!("./test_keys/rsa_pkcs8_unencrypted");
|
||||
// for whatever reason pkcs8 + rsa does not include the comment in the public key
|
||||
let public_key =
|
||||
include_str!("./test_keys/rsa_pkcs8_unencrypted.pub").replace("testkey", "");
|
||||
let public_key = public_key.trim();
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_rsa_pkcs8_encrypted() {
|
||||
let private_key = include_str!("./test_keys/rsa_pkcs8_encrypted");
|
||||
let public_key = include_str!("./test_keys/rsa_pkcs8_encrypted.pub").replace("testkey", "");
|
||||
let public_key = public_key.trim();
|
||||
let result = import_key(private_key.to_string(), "password".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::Success);
|
||||
assert_eq!(result.ssh_key.unwrap().public_key, public_key);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_ed25519_openssh_encrypted_wrong_password() {
|
||||
let private_key = include_str!("./test_keys/ed25519_openssh_encrypted");
|
||||
let result = import_key(private_key.to_string(), "wrongpassword".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::WrongPassword);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_non_key_error() {
|
||||
let result = import_key("not a key".to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::ParsingError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_ecdsa_error() {
|
||||
let private_key = include_str!("./test_keys/ecdsa_openssh_unencrypted");
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType);
|
||||
}
|
||||
|
||||
// Putty-exported keys should be supported, but are not due to a parser incompatibility.
|
||||
// Should this test start failing, please change it to expect a correct key, and
|
||||
// make sure the documentation support for putty-exported keys this is updated.
|
||||
// https://bitwarden.atlassian.net/browse/PM-14989
|
||||
#[test]
|
||||
fn import_key_ed25519_putty() {
|
||||
let private_key = include_str!("./test_keys/ed25519_putty_openssh_unencrypted");
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::ParsingError);
|
||||
}
|
||||
|
||||
// Putty-exported keys should be supported, but are not due to a parser incompatibility.
|
||||
// Should this test start failing, please change it to expect a correct key, and
|
||||
// make sure the documentation support for putty-exported keys this is updated.
|
||||
// https://bitwarden.atlassian.net/browse/PM-14989
|
||||
#[test]
|
||||
fn import_key_rsa_openssh_putty() {
|
||||
let private_key = include_str!("./test_keys/rsa_putty_openssh_unencrypted");
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::ParsingError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_key_rsa_pkcs8_putty() {
|
||||
let private_key = include_str!("./test_keys/rsa_putty_pkcs1_unencrypted");
|
||||
let result = import_key(private_key.to_string(), "".to_string()).unwrap();
|
||||
assert_eq!(result.status, SshKeyImportStatus::UnsupportedKeyType);
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,9 @@ mod platform_ssh_agent;
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
mod peercred_unix_listener_stream;
|
||||
|
||||
pub mod generator;
|
||||
pub mod importer;
|
||||
pub mod peerinfo;
|
||||
mod request_parser;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BitwardenDesktopAgent {
|
||||
keystore: ssh_agent::KeyStore,
|
||||
@@ -36,19 +36,37 @@ pub struct SshAgentUIRequest {
|
||||
pub cipher_id: Option<String>,
|
||||
pub process_name: String,
|
||||
pub is_list: bool,
|
||||
pub namespace: Option<String>,
|
||||
pub is_forwarding: bool,
|
||||
}
|
||||
|
||||
impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
|
||||
async fn confirm(&self, ssh_key: Key, info: &peerinfo::models::PeerInfo) -> bool {
|
||||
async fn confirm(&self, ssh_key: Key, data: &[u8], info: &peerinfo::models::PeerInfo) -> bool {
|
||||
if !self.is_running() {
|
||||
println!("[BitwardenDesktopAgent] Agent is not running, but tried to call confirm");
|
||||
return false;
|
||||
}
|
||||
|
||||
let request_id = self.get_request_id().await;
|
||||
let request_data = match request_parser::parse_request(data) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
println!("[SSH Agent] Error while parsing request: {}", e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let namespace = match request_data {
|
||||
request_parser::SshAgentSignRequest::SshSigRequest(ref req) => {
|
||||
Some(req.namespace.clone())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
println!(
|
||||
"[SSH Agent] Confirming request from application: {}",
|
||||
info.process_name()
|
||||
"[SSH Agent] Confirming request from application: {}, is_forwarding: {}, namespace: {}",
|
||||
info.process_name(),
|
||||
info.is_forwarding(),
|
||||
namespace.clone().unwrap_or_default(),
|
||||
);
|
||||
|
||||
let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe();
|
||||
@@ -58,6 +76,8 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
|
||||
cipher_id: Some(ssh_key.cipher_uuid.clone()),
|
||||
process_name: info.process_name().to_string(),
|
||||
is_list: false,
|
||||
namespace,
|
||||
is_forwarding: info.is_forwarding(),
|
||||
})
|
||||
.await
|
||||
.expect("Should send request to ui");
|
||||
@@ -82,6 +102,8 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
|
||||
cipher_id: None,
|
||||
process_name: info.process_name().to_string(),
|
||||
is_list: true,
|
||||
namespace: None,
|
||||
is_forwarding: info.is_forwarding(),
|
||||
};
|
||||
self.show_ui_request_tx
|
||||
.send(message)
|
||||
@@ -94,6 +116,17 @@ impl ssh_agent::Agent<peerinfo::models::PeerInfo> for BitwardenDesktopAgent {
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
async fn set_is_forwarding(
|
||||
&self,
|
||||
is_forwarding: bool,
|
||||
connection_info: &peerinfo::models::PeerInfo,
|
||||
) {
|
||||
// is_forwarding can only be added but never removed from a connection
|
||||
if is_forwarding {
|
||||
connection_info.set_forwarding(is_forwarding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BitwardenDesktopAgent {
|
||||
@@ -129,7 +162,7 @@ impl BitwardenDesktopAgent {
|
||||
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
for (key, name, cipher_id) in new_keys.iter() {
|
||||
match parse_key_safe(&key) {
|
||||
match parse_key_safe(key) {
|
||||
Ok(private_key) => {
|
||||
let public_key_bytes = private_key
|
||||
.public_key()
|
||||
@@ -187,10 +220,8 @@ impl BitwardenDesktopAgent {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let request_id = self
|
||||
.request_id
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
request_id
|
||||
self.request_id
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
|
||||
@@ -61,7 +61,7 @@ impl NamedPipeServerStream {
|
||||
}
|
||||
};
|
||||
|
||||
let peer_info = peerinfo::gather::get_peer_info(pid as u32);
|
||||
let peer_info = peerinfo::gather::get_peer_info(pid);
|
||||
let peer_info = match peer_info {
|
||||
Err(err) => {
|
||||
println!("Failed getting process info for pid {} {}", pid, err);
|
||||
|
||||
@@ -31,26 +31,15 @@ impl Stream for PeercredUnixListenerStream {
|
||||
Ok(peer) => match peer.pid() {
|
||||
Some(pid) => pid,
|
||||
None => {
|
||||
return Poll::Ready(Some(Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Failed to get peer PID",
|
||||
))));
|
||||
return Poll::Ready(Some(Ok((stream, PeerInfo::unknown()))));
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
return Poll::Ready(Some(Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Failed to get peer credentials: {}", err),
|
||||
))));
|
||||
}
|
||||
Err(_) => return Poll::Ready(Some(Ok((stream, PeerInfo::unknown())))),
|
||||
};
|
||||
let peer_info = peerinfo::gather::get_peer_info(pid as u32);
|
||||
match peer_info {
|
||||
Ok(info) => Poll::Ready(Some(Ok((stream, info)))),
|
||||
Err(err) => Poll::Ready(Some(Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Failed to get peer info: {}", err),
|
||||
)))),
|
||||
Err(_) => Poll::Ready(Some(Ok((stream, PeerInfo::unknown())))),
|
||||
}
|
||||
}
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
/**
|
||||
* Peerinfo represents the information of a peer process connecting over a socket.
|
||||
* This can be later extended to include more information (icon, app name) for the corresponding application.
|
||||
@@ -7,6 +9,7 @@ pub struct PeerInfo {
|
||||
uid: u32,
|
||||
pid: u32,
|
||||
process_name: String,
|
||||
is_forwarding: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl PeerInfo {
|
||||
@@ -15,6 +18,16 @@ impl PeerInfo {
|
||||
uid,
|
||||
pid,
|
||||
process_name,
|
||||
is_forwarding: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unknown() -> Self {
|
||||
Self {
|
||||
uid: 0,
|
||||
pid: 0,
|
||||
process_name: "Unknown application".to_string(),
|
||||
is_forwarding: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +42,14 @@ impl PeerInfo {
|
||||
pub fn process_name(&self) -> &str {
|
||||
&self.process_name
|
||||
}
|
||||
|
||||
pub fn is_forwarding(&self) -> bool {
|
||||
self.is_forwarding
|
||||
.load(std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_forwarding(&self, value: bool) {
|
||||
self.is_forwarding
|
||||
.store(value, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use bytes::{Buf, Bytes};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SshSigRequest {
|
||||
pub namespace: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SignRequest {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SshAgentSignRequest {
|
||||
SshSigRequest(SshSigRequest),
|
||||
SignRequest(SignRequest),
|
||||
}
|
||||
|
||||
pub(crate) fn parse_request(data: &[u8]) -> Result<SshAgentSignRequest, anyhow::Error> {
|
||||
let mut data = Bytes::copy_from_slice(data);
|
||||
let magic_header = "SSHSIG";
|
||||
let header = data.split_to(magic_header.len());
|
||||
|
||||
// sshsig; based on https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig
|
||||
if header == magic_header.as_bytes() {
|
||||
let _version = data.get_u32();
|
||||
|
||||
// read until null byte
|
||||
let namespace = data
|
||||
.into_iter()
|
||||
.take_while(|&x| x != 0)
|
||||
.collect::<Vec<u8>>();
|
||||
let namespace =
|
||||
String::from_utf8(namespace).map_err(|_| anyhow::anyhow!("Invalid namespace"))?;
|
||||
|
||||
Ok(SshAgentSignRequest::SshSigRequest(SshSigRequest {
|
||||
namespace,
|
||||
}))
|
||||
} else {
|
||||
// regular sign request
|
||||
Ok(SshAgentSignRequest::SignRequest(SignRequest {}))
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ impl BitwardenDesktopAgent {
|
||||
show_ui_request_tx: auth_request_tx,
|
||||
get_ui_response_rx: auth_response_rx,
|
||||
request_id: Arc::new(AtomicU32::new(0)),
|
||||
needs_unlock: Arc::new(AtomicBool::new(false)),
|
||||
needs_unlock: Arc::new(AtomicBool::new(true)),
|
||||
is_running: Arc::new(AtomicBool::new(false)),
|
||||
};
|
||||
let cloned_agent_state = agent.clone();
|
||||
@@ -47,11 +47,21 @@ impl BitwardenDesktopAgent {
|
||||
return;
|
||||
}
|
||||
};
|
||||
ssh_agent_directory
|
||||
.join(".bitwarden-ssh-agent.sock")
|
||||
.to_str()
|
||||
.expect("Path should be valid")
|
||||
.to_owned()
|
||||
|
||||
let is_flatpak = std::env::var("container") == Ok("flatpak".to_string());
|
||||
if !is_flatpak {
|
||||
ssh_agent_directory
|
||||
.join(".bitwarden-ssh-agent.sock")
|
||||
.to_str()
|
||||
.expect("Path should be valid")
|
||||
.to_owned()
|
||||
} else {
|
||||
ssh_agent_directory
|
||||
.join(".var/app/com.bitwarden.desktop/data/.bitwarden-ssh-agent.sock")
|
||||
.to_str()
|
||||
.expect("Path should be valid")
|
||||
.to_owned()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "macos_provider"
|
||||
license = "GPL-3.0"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[[bin]]
|
||||
name = "uniffi-bindgen"
|
||||
@@ -16,15 +16,15 @@ bench = false
|
||||
[dependencies]
|
||||
desktop_core = { path = "../core" }
|
||||
futures = "=0.3.31"
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0.205", features = ["derive"] }
|
||||
serde_json = "1.0.122"
|
||||
tokio = { version = "1.39.2", features = ["sync"] }
|
||||
tokio-util = "0.7.11"
|
||||
uniffi = { version = "0.28.0", features = ["cli"] }
|
||||
log = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tokio-util = { workspace = true }
|
||||
uniffi = { version = "=0.28.3", features = ["cli"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
oslog = "0.2.0"
|
||||
oslog = "=0.2.0"
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { version = "0.28.0", features = ["build"] }
|
||||
uniffi = { version = "=0.28.3", features = ["build"] }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
exclude = ["index.node"]
|
||||
license = "GPL-3.0"
|
||||
name = "desktop_napi"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
exclude = ["index.node"]
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
@@ -16,18 +16,18 @@ manual_test = []
|
||||
[dependencies]
|
||||
base64 = "=0.22.1"
|
||||
hex = "=0.4.3"
|
||||
anyhow = "=1.0.94"
|
||||
anyhow = { workspace = true }
|
||||
desktop_core = { path = "../core" }
|
||||
napi = { version = "=2.16.13", features = ["async"] }
|
||||
napi = { version = "=2.16.15", features = ["async"] }
|
||||
napi-derive = "=2.16.13"
|
||||
serde = { version = "1.0.209", features = ["derive"] }
|
||||
serde_json = "1.0.127"
|
||||
tokio = { version = "=1.41.1" }
|
||||
tokio-util = "=0.7.12"
|
||||
tokio-stream = "=0.1.15"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
tokio-util = { workspace = true }
|
||||
tokio-stream = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-registry = "=0.3.0"
|
||||
windows-registry = "=0.4.0"
|
||||
|
||||
[build-dependencies]
|
||||
napi-build = "=2.1.3"
|
||||
napi-build = "=2.1.4"
|
||||
|
||||
25
apps/desktop/desktop_native/napi/index.d.ts
vendored
25
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -51,30 +51,19 @@ export declare namespace sshagent {
|
||||
publicKey: string
|
||||
keyFingerprint: string
|
||||
}
|
||||
export const enum SshKeyImportStatus {
|
||||
/** ssh key was parsed correctly and will be returned in the result */
|
||||
Success = 0,
|
||||
/** ssh key was parsed correctly but is encrypted and requires a password */
|
||||
PasswordRequired = 1,
|
||||
/** ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect */
|
||||
WrongPassword = 2,
|
||||
/** ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key */
|
||||
ParsingError = 3,
|
||||
/** ssh key type is not supported (e.g. ecdsa) */
|
||||
UnsupportedKeyType = 4
|
||||
export interface SshUiRequest {
|
||||
cipherId?: string
|
||||
isList: boolean
|
||||
processName: string
|
||||
isForwarding: boolean
|
||||
namespace?: string
|
||||
}
|
||||
export interface SshKeyImportResult {
|
||||
status: SshKeyImportStatus
|
||||
sshKey?: SshKey
|
||||
}
|
||||
export function serve(callback: (err: Error | null, arg0: string | undefined | null, arg1: boolean, arg2: string) => any): Promise<SshAgentState>
|
||||
export function serve(callback: (err: Error | null, arg: SshUiRequest) => any): Promise<SshAgentState>
|
||||
export function stop(agentState: SshAgentState): void
|
||||
export function isRunning(agentState: SshAgentState): boolean
|
||||
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
|
||||
export function lock(agentState: SshAgentState): void
|
||||
export function importKey(encodedKey: string, password: string): SshKeyImportResult
|
||||
export function clearKeys(agentState: SshAgentState): void
|
||||
export function generateKeypair(keyAlgorithm: string): Promise<SshKey>
|
||||
export class SshAgentState { }
|
||||
}
|
||||
export declare namespace processisolations {
|
||||
|
||||
@@ -1,209 +1,132 @@
|
||||
const { existsSync, readFileSync } = require('fs')
|
||||
const { join } = require('path')
|
||||
const { existsSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
|
||||
const { platform, arch } = process
|
||||
const { platform, arch } = process;
|
||||
|
||||
let nativeBinding = null
|
||||
let localFileExisted = false
|
||||
let loadError = null
|
||||
let nativeBinding = null;
|
||||
let localFileExisted = false;
|
||||
let loadError = null;
|
||||
|
||||
function isMusl() {
|
||||
// For Node 10
|
||||
if (!process.report || typeof process.report.getReport !== 'function') {
|
||||
try {
|
||||
return readFileSync('/usr/bin/ldd', 'utf8').includes('musl')
|
||||
} catch (e) {
|
||||
return true
|
||||
function loadFirstAvailable(localFiles, nodeModule) {
|
||||
for (const localFile of localFiles) {
|
||||
if (existsSync(join(__dirname, localFile))) {
|
||||
return require(`./${localFile}`);
|
||||
}
|
||||
} else {
|
||||
const { glibcVersionRuntime } = process.report.getReport().header
|
||||
return !glibcVersionRuntime
|
||||
}
|
||||
|
||||
require(nodeModule);
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
case "android":
|
||||
switch (arch) {
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(join(__dirname, 'desktop_napi.android-arm64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.android-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-android-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(join(__dirname, 'desktop_napi.android-arm-eabi.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.android-arm-eabi.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-android-arm-eabi')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case "arm64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.android-arm64.node"],
|
||||
"@bitwarden/desktop-napi-android-arm64",
|
||||
);
|
||||
break;
|
||||
case "arm":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.android-arm.node"],
|
||||
"@bitwarden/desktop-napi-android-arm",
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Android ${arch}`)
|
||||
throw new Error(`Unsupported architecture on Android ${arch}`);
|
||||
}
|
||||
break
|
||||
case 'win32':
|
||||
break;
|
||||
case "win32":
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.win32-x64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.win32-x64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-win32-x64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'ia32':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.win32-ia32-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.win32-ia32-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-win32-ia32-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.win32-arm64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.win32-arm64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-win32-arm64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case "x64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.win32-x64-msvc.node"],
|
||||
"@bitwarden/desktop-napi-win32-x64-msvc",
|
||||
);
|
||||
break;
|
||||
case "ia32":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.win32-ia32-msvc.node"],
|
||||
"@bitwarden/desktop-napi-win32-ia32-msvc",
|
||||
);
|
||||
break;
|
||||
case "arm64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.win32-arm64-msvc.node"],
|
||||
"@bitwarden/desktop-napi-win32-arm64-msvc",
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
||||
throw new Error(`Unsupported architecture on Windows: ${arch}`);
|
||||
}
|
||||
break
|
||||
case 'darwin':
|
||||
break;
|
||||
case "darwin":
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(join(__dirname, 'desktop_napi.darwin-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.darwin-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-darwin-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.darwin-arm64.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.darwin-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-darwin-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case "x64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.darwin-x64.node"],
|
||||
"@bitwarden/desktop-napi-darwin-x64",
|
||||
);
|
||||
break;
|
||||
case "arm64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.darwin-arm64.node"],
|
||||
"@bitwarden/desktop-napi-darwin-arm64",
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
||||
throw new Error(`Unsupported architecture on macOS: ${arch}`);
|
||||
}
|
||||
break
|
||||
case 'freebsd':
|
||||
if (arch !== 'x64') {
|
||||
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
||||
}
|
||||
localFileExisted = existsSync(join(__dirname, 'desktop_napi.freebsd-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.freebsd-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-freebsd-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'linux':
|
||||
break;
|
||||
case "freebsd":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.freebsd-x64.node"],
|
||||
"@bitwarden/desktop-napi-freebsd-x64",
|
||||
);
|
||||
break;
|
||||
case "linux":
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.linux-x64-musl.node')
|
||||
)
|
||||
case "x64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-x64-musl.node", "desktop_napi.linux-x64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-x64-musl",
|
||||
);
|
||||
break;
|
||||
case "arm64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-arm64-musl.node", "desktop_napi.linux-arm64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm64-musl",
|
||||
);
|
||||
break;
|
||||
case "arm":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-arm-musl.node", "desktop_napi.linux-arm-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm-musl",
|
||||
);
|
||||
localFileExisted = existsSync(join(__dirname, "desktop_napi.linux-arm-gnueabihf.node"));
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.linux-x64-musl.node')
|
||||
nativeBinding = require("./desktop_napi.linux-arm-gnueabihf.node");
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-linux-x64-musl')
|
||||
nativeBinding = require("@bitwarden/desktop-napi-linux-arm-gnueabihf");
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
loadError = e;
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.linux-arm64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.linux-arm64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-linux-arm64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.linux-arm-gnueabihf.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.linux-arm-gnueabihf.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-linux-arm-gnueabihf')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
||||
throw new Error(`Unsupported architecture on Linux: ${arch}`);
|
||||
}
|
||||
break
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
||||
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`);
|
||||
}
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadError) {
|
||||
throw loadError
|
||||
throw loadError;
|
||||
}
|
||||
throw new Error(`Failed to load native binding`)
|
||||
throw new Error(`Failed to load native binding`);
|
||||
}
|
||||
|
||||
module.exports = nativeBinding
|
||||
module.exports = nativeBinding;
|
||||
|
||||
@@ -89,11 +89,9 @@ pub mod biometrics {
|
||||
account: String,
|
||||
key_material: Option<KeyMaterial>,
|
||||
) -> napi::Result<String> {
|
||||
let result =
|
||||
Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into()))
|
||||
.await
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()));
|
||||
result
|
||||
Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into()))
|
||||
.await
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
/// Derives key material from biometric data. Returns a string encoded with a
|
||||
@@ -184,70 +182,18 @@ pub mod sshagent {
|
||||
pub key_fingerprint: String,
|
||||
}
|
||||
|
||||
impl From<desktop_core::ssh_agent::importer::SshKey> for SshKey {
|
||||
fn from(key: desktop_core::ssh_agent::importer::SshKey) -> Self {
|
||||
SshKey {
|
||||
private_key: key.private_key,
|
||||
public_key: key.public_key,
|
||||
key_fingerprint: key.key_fingerprint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub enum SshKeyImportStatus {
|
||||
/// ssh key was parsed correctly and will be returned in the result
|
||||
Success,
|
||||
/// ssh key was parsed correctly but is encrypted and requires a password
|
||||
PasswordRequired,
|
||||
/// ssh key was parsed correctly, and a password was provided when calling the import, but it was incorrect
|
||||
WrongPassword,
|
||||
/// ssh key could not be parsed, either due to an incorrect / unsupported format (pkcs#8) or key type (ecdsa), or because the input is not an ssh key
|
||||
ParsingError,
|
||||
/// ssh key type is not supported (e.g. ecdsa)
|
||||
UnsupportedKeyType,
|
||||
}
|
||||
|
||||
impl From<desktop_core::ssh_agent::importer::SshKeyImportStatus> for SshKeyImportStatus {
|
||||
fn from(status: desktop_core::ssh_agent::importer::SshKeyImportStatus) -> Self {
|
||||
match status {
|
||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::Success => {
|
||||
SshKeyImportStatus::Success
|
||||
}
|
||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::PasswordRequired => {
|
||||
SshKeyImportStatus::PasswordRequired
|
||||
}
|
||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::WrongPassword => {
|
||||
SshKeyImportStatus::WrongPassword
|
||||
}
|
||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::ParsingError => {
|
||||
SshKeyImportStatus::ParsingError
|
||||
}
|
||||
desktop_core::ssh_agent::importer::SshKeyImportStatus::UnsupportedKeyType => {
|
||||
SshKeyImportStatus::UnsupportedKeyType
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct SshKeyImportResult {
|
||||
pub status: SshKeyImportStatus,
|
||||
pub ssh_key: Option<SshKey>,
|
||||
}
|
||||
|
||||
impl From<desktop_core::ssh_agent::importer::SshKeyImportResult> for SshKeyImportResult {
|
||||
fn from(result: desktop_core::ssh_agent::importer::SshKeyImportResult) -> Self {
|
||||
SshKeyImportResult {
|
||||
status: result.status.into(),
|
||||
ssh_key: result.ssh_key.map(|k| k.into()),
|
||||
}
|
||||
}
|
||||
pub struct SshUIRequest {
|
||||
pub cipher_id: Option<String>,
|
||||
pub is_list: bool,
|
||||
pub process_name: String,
|
||||
pub is_forwarding: bool,
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn serve(
|
||||
callback: ThreadsafeFunction<(Option<String>, bool, String), CalleeHandled>,
|
||||
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>,
|
||||
) -> napi::Result<SshAgentState> {
|
||||
let (auth_request_tx, mut auth_request_rx) =
|
||||
tokio::sync::mpsc::channel::<desktop_core::ssh_agent::SshAgentUIRequest>(32);
|
||||
@@ -264,11 +210,13 @@ pub mod sshagent {
|
||||
let auth_response_tx_arc = cloned_response_tx_arc;
|
||||
let callback = cloned_callback;
|
||||
let promise_result: Result<Promise<bool>, napi::Error> = callback
|
||||
.call_async(Ok((
|
||||
request.cipher_id,
|
||||
request.is_list,
|
||||
request.process_name,
|
||||
)))
|
||||
.call_async(Ok(SshUIRequest {
|
||||
cipher_id: request.cipher_id,
|
||||
is_list: request.is_list,
|
||||
process_name: request.process_name,
|
||||
is_forwarding: request.is_forwarding,
|
||||
namespace: request.namespace,
|
||||
}))
|
||||
.await;
|
||||
match promise_result {
|
||||
Ok(promise_result) => match promise_result.await {
|
||||
@@ -350,13 +298,6 @@ pub mod sshagent {
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn import_key(encoded_key: String, password: String) -> napi::Result<SshKeyImportResult> {
|
||||
let result = desktop_core::ssh_agent::importer::import_key(encoded_key, password)
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> {
|
||||
let bitwarden_agent_state = &mut agent_state.state;
|
||||
@@ -364,14 +305,6 @@ pub mod sshagent {
|
||||
.clear_keys()
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub async fn generate_keypair(key_algorithm: String) -> napi::Result<SshKey> {
|
||||
desktop_core::ssh_agent::generator::generate_keypair(key_algorithm)
|
||||
.await
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
.map(|k| k.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
@@ -409,8 +342,8 @@ pub mod powermonitors {
|
||||
.await
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
||||
tokio::spawn(async move {
|
||||
while let Some(message) = rx.recv().await {
|
||||
callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
while let Some(()) = rx.recv().await {
|
||||
callback.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
@@ -860,6 +793,6 @@ pub mod crypto {
|
||||
desktop_core::crypto::argon2(&secret, &salt, iterations, memory, parallelism)
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
.map(|v| v.to_vec())
|
||||
.map(|v| Buffer::from(v))
|
||||
.map(Buffer::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
license = "GPL-3.0"
|
||||
name = "desktop_objc"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anyhow = "=1.0.94"
|
||||
thiserror = "=1.0.69"
|
||||
tokio = "1.39.1"
|
||||
anyhow = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "=0.9.4"
|
||||
core-foundation = "=0.10.0"
|
||||
|
||||
[build-dependencies]
|
||||
cc = "1.0.104"
|
||||
glob = "0.3.1"
|
||||
cc = "=1.2.4"
|
||||
glob = "=0.3.2"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use glob::glob;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn main() {
|
||||
use glob::glob;
|
||||
let mut builder = cc::Build::new();
|
||||
|
||||
// Auto compile all .m files in the src/native directory
|
||||
|
||||
@@ -68,13 +68,13 @@ mod objc {
|
||||
|
||||
use super::*;
|
||||
|
||||
extern "C" {
|
||||
pub fn runCommand(context: *mut c_void, value: *const c_char);
|
||||
pub fn freeObjCString(value: &ObjCString);
|
||||
unsafe extern "C" {
|
||||
pub unsafe fn runCommand(context: *mut c_void, value: *const c_char);
|
||||
pub unsafe fn freeObjCString(value: &ObjCString);
|
||||
}
|
||||
|
||||
/// This function is called from the ObjC code to return the output of the command
|
||||
#[no_mangle]
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn commandReturn(context: &mut CommandContext, value: ObjCString) -> bool {
|
||||
let value: String = match value.try_into() {
|
||||
Ok(value) => value,
|
||||
@@ -100,7 +100,7 @@ mod objc {
|
||||
}
|
||||
};
|
||||
|
||||
return true;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ pub async fn run_command(input: String) -> Result<String> {
|
||||
unsafe { objc::runCommand(context.as_ptr(), c_input.as_ptr()) };
|
||||
|
||||
// Convert output from ObjC code to Rust string
|
||||
let objc_output = rx.await?.try_into()?;
|
||||
let objc_output = rx.await?;
|
||||
|
||||
// Convert output from ObjC code to Rust string
|
||||
// let objc_output = output.try_into()?;
|
||||
|
||||
@@ -30,21 +30,24 @@ void runSync(void* context, NSDictionary *params) {
|
||||
[mappedCredentials addObject:credential];
|
||||
}
|
||||
|
||||
if ([type isEqualToString:@"fido2"]) {
|
||||
NSString *cipherId = credential[@"cipherId"];
|
||||
NSString *rpId = credential[@"rpId"];
|
||||
NSString *userName = credential[@"userName"];
|
||||
NSData *credentialId = decodeBase64URL(credential[@"credentialId"]);
|
||||
NSData *userHandle = decodeBase64URL(credential[@"userHandle"]);
|
||||
if (@available(macos 14, *)) {
|
||||
if ([type isEqualToString:@"fido2"]) {
|
||||
NSString *cipherId = credential[@"cipherId"];
|
||||
NSString *rpId = credential[@"rpId"];
|
||||
NSString *userName = credential[@"userName"];
|
||||
NSData *credentialId = decodeBase64URL(credential[@"credentialId"]);
|
||||
NSData *userHandle = decodeBase64URL(credential[@"userHandle"]);
|
||||
|
||||
ASPasskeyCredentialIdentity *credential = [[ASPasskeyCredentialIdentity alloc]
|
||||
initWithRelyingPartyIdentifier:rpId
|
||||
userName:userName
|
||||
credentialID:credentialId
|
||||
userHandle:userHandle
|
||||
recordIdentifier:cipherId];
|
||||
Class passkeyCredentialIdentityClass = NSClassFromString(@"ASPasskeyCredentialIdentity");
|
||||
id credential = [[passkeyCredentialIdentityClass alloc]
|
||||
initWithRelyingPartyIdentifier:rpId
|
||||
userName:userName
|
||||
credentialID:credentialId
|
||||
userHandle:userHandle
|
||||
recordIdentifier:cipherId];
|
||||
|
||||
[mappedCredentials addObject:credential];
|
||||
[mappedCredentials addObject:credential];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
[package]
|
||||
edition = "2021"
|
||||
exclude = ["index.node"]
|
||||
license = "GPL-3.0"
|
||||
name = "desktop_proxy"
|
||||
version = "0.0.0"
|
||||
publish = false
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
anyhow = "=1.0.94"
|
||||
desktop_core = { path = "../core", default-features = false }
|
||||
anyhow = { workspace = true }
|
||||
desktop_core = { path = "../core" }
|
||||
futures = "=0.3.31"
|
||||
log = "=0.4.22"
|
||||
log = { workspace = true }
|
||||
simplelog = "=0.12.2"
|
||||
tokio = { version = "=1.41.1", features = ["io-std", "io-util", "macros", "rt"] }
|
||||
tokio-util = { version = "=0.7.12", features = ["codec"] }
|
||||
tokio = { workspace = true, features = ["io-std", "io-util", "macros", "rt"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
embed_plist = "=1.2.2"
|
||||
|
||||
@@ -5,6 +5,9 @@ use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use log::*;
|
||||
use tokio_util::codec::LengthDelimitedCodec;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
embed_plist::embed_info_plist!("../../../resources/info.desktop_proxy.plist");
|
||||
|
||||
@@ -49,6 +52,9 @@ fn init_logging(log_path: &Path, console_level: LevelFilter, file_level: LevelFi
|
||||
///
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
let should_foreground = windows::allow_foreground();
|
||||
|
||||
let sock_path = desktop_core::ipc::path("bitwarden");
|
||||
|
||||
let log_path = {
|
||||
@@ -142,6 +148,9 @@ async fn main() {
|
||||
|
||||
// Listen to stdin and send messages to ipc processor.
|
||||
msg = stdin.next() => {
|
||||
#[cfg(target_os = "windows")]
|
||||
should_foreground.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
match msg {
|
||||
Some(Ok(msg)) => {
|
||||
let m = String::from_utf8(msg.to_vec()).unwrap();
|
||||
|
||||
23
apps/desktop/desktop_native/proxy/src/windows.rs
Normal file
23
apps/desktop/desktop_native/proxy/src/windows.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
pub fn allow_foreground() -> Arc<AtomicBool> {
|
||||
let should_foreground = Arc::new(AtomicBool::new(false));
|
||||
let should_foreground_clone = should_foreground.clone();
|
||||
let _ = std::thread::spawn(move || loop {
|
||||
if !should_foreground_clone.load(Ordering::Relaxed) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
should_foreground_clone.store(false, Ordering::Relaxed);
|
||||
|
||||
for _ in 0..60 {
|
||||
desktop_core::biometric::windows_focus::focus_security_prompt();
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
}
|
||||
});
|
||||
|
||||
should_foreground
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "windows-plugin-authenticator"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
publish = { workspace = true }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||
bindgen = "0.71.1"
|
||||
@@ -0,0 +1,23 @@
|
||||
# windows-plugin-authenticator
|
||||
|
||||
This is an internal crate that's meant to be a safe abstraction layer over the generated Rust bindings for the Windows WebAuthn Plugin Authenticator API's.
|
||||
|
||||
You can find more information about the Windows WebAuthn API's [here](https://github.com/microsoft/webauthn).
|
||||
|
||||
## Building
|
||||
|
||||
To build this crate, set the following environment variables:
|
||||
|
||||
- `LIBCLANG_PATH` -> the path to the `bin` directory of your LLVM install ([more info](https://rust-lang.github.io/rust-bindgen/requirements.html?highlight=libclang_path#installing-clang))
|
||||
|
||||
### Bash Example
|
||||
|
||||
```
|
||||
export LIBCLANG_PATH='C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
|
||||
```
|
||||
|
||||
### PowerShell Example
|
||||
|
||||
```
|
||||
$env:LIBCLANG_PATH = 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
windows();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn windows() {
|
||||
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
|
||||
|
||||
let bindings = bindgen::Builder::default()
|
||||
.header("pluginauthenticator.hpp")
|
||||
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
|
||||
.generate()
|
||||
.expect("Unable to generate bindings.");
|
||||
|
||||
bindings
|
||||
.write_to_file(format!(
|
||||
"{}\\windows_pluginauthenticator_bindings.rs",
|
||||
out_dir
|
||||
))
|
||||
.expect("Couldn't write bindings.");
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
Bitwarden's pluginauthenticator.hpp
|
||||
|
||||
Source: https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h
|
||||
|
||||
This is a C++ header file, so the extension has been manually
|
||||
changed from `.h` to `.hpp`, so bindgen will automatically
|
||||
generate the correct C++ bindings.
|
||||
|
||||
More Info: https://rust-lang.github.io/rust-bindgen/cpp.html
|
||||
*/
|
||||
|
||||
/* this ALWAYS GENERATED file contains the definitions for the interfaces */
|
||||
|
||||
/* File created by MIDL compiler version 8.01.0628 */
|
||||
/* @@MIDL_FILE_HEADING( ) */
|
||||
|
||||
/* verify that the <rpcndr.h> version is high enough to compile this file*/
|
||||
#ifndef __REQUIRED_RPCNDR_H_VERSION__
|
||||
#define __REQUIRED_RPCNDR_H_VERSION__ 501
|
||||
#endif
|
||||
|
||||
/* verify that the <rpcsal.h> version is high enough to compile this file*/
|
||||
#ifndef __REQUIRED_RPCSAL_H_VERSION__
|
||||
#define __REQUIRED_RPCSAL_H_VERSION__ 100
|
||||
#endif
|
||||
|
||||
#include "rpc.h"
|
||||
#include "rpcndr.h"
|
||||
|
||||
#ifndef __RPCNDR_H_VERSION__
|
||||
#error this stub requires an updated version of <rpcndr.h>
|
||||
#endif /* __RPCNDR_H_VERSION__ */
|
||||
|
||||
#ifndef COM_NO_WINDOWS_H
|
||||
#include "windows.h"
|
||||
#include "ole2.h"
|
||||
#endif /*COM_NO_WINDOWS_H*/
|
||||
|
||||
#ifndef __pluginauthenticator_h__
|
||||
#define __pluginauthenticator_h__
|
||||
|
||||
#if defined(_MSC_VER) && (_MSC_VER >= 1020)
|
||||
#pragma once
|
||||
#endif
|
||||
|
||||
#ifndef DECLSPEC_XFGVIRT
|
||||
#if defined(_CONTROL_FLOW_GUARD_XFG)
|
||||
#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func))
|
||||
#else
|
||||
#define DECLSPEC_XFGVIRT(base, func)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
/* Forward Declarations */
|
||||
|
||||
#ifndef __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
|
||||
#define __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
|
||||
typedef interface EXPERIMENTAL_IPluginAuthenticator EXPERIMENTAL_IPluginAuthenticator;
|
||||
|
||||
#endif /* __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ */
|
||||
|
||||
/* header files for imported files */
|
||||
#include "oaidl.h"
|
||||
#include "webauthn.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"{
|
||||
#endif
|
||||
|
||||
/* interface __MIDL_itf_pluginauthenticator_0000_0000 */
|
||||
/* [local] */
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST
|
||||
{
|
||||
HWND hWnd;
|
||||
GUID transactionId;
|
||||
DWORD cbRequestSignature;
|
||||
/* [size_is] */ byte *pbRequestSignature;
|
||||
DWORD cbEncodedRequest;
|
||||
/* [size_is] */ byte *pbEncodedRequest;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE
|
||||
{
|
||||
DWORD cbEncodedResponse;
|
||||
/* [size_is] */ byte *pbEncodedResponse;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
|
||||
{
|
||||
GUID transactionId;
|
||||
DWORD cbRequestSignature;
|
||||
/* [size_is] */ byte *pbRequestSignature;
|
||||
} EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
|
||||
|
||||
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec;
|
||||
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec;
|
||||
|
||||
#ifndef __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
|
||||
#define __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
|
||||
|
||||
/* interface EXPERIMENTAL_IPluginAuthenticator */
|
||||
/* [unique][version][uuid][object] */
|
||||
|
||||
EXTERN_C const IID IID_EXPERIMENTAL_IPluginAuthenticator;
|
||||
|
||||
#if defined(__cplusplus) && !defined(CINTERFACE)
|
||||
|
||||
MIDL_INTERFACE("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")
|
||||
EXPERIMENTAL_IPluginAuthenticator : public IUnknown
|
||||
{
|
||||
public:
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginMakeCredential(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginGetAssertion(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
|
||||
|
||||
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginCancelOperation(
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0;
|
||||
|
||||
};
|
||||
|
||||
#else /* C style interface */
|
||||
|
||||
typedef struct EXPERIMENTAL_IPluginAuthenticatorVtbl
|
||||
{
|
||||
BEGIN_INTERFACE
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, QueryInterface)
|
||||
HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in REFIID riid,
|
||||
/* [annotation][iid_is][out] */
|
||||
_COM_Outptr_ void **ppvObject);
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, AddRef)
|
||||
ULONG ( STDMETHODCALLTYPE *AddRef )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
|
||||
|
||||
DECLSPEC_XFGVIRT(IUnknown, Release)
|
||||
ULONG ( STDMETHODCALLTYPE *Release )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginMakeCredential)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginMakeCredential )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginGetAssertion)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginGetAssertion )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
|
||||
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
|
||||
|
||||
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginCancelOperation)
|
||||
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginCancelOperation )(
|
||||
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
|
||||
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request);
|
||||
|
||||
END_INTERFACE
|
||||
} EXPERIMENTAL_IPluginAuthenticatorVtbl;
|
||||
|
||||
interface EXPERIMENTAL_IPluginAuthenticator
|
||||
{
|
||||
CONST_VTBL struct EXPERIMENTAL_IPluginAuthenticatorVtbl *lpVtbl;
|
||||
};
|
||||
|
||||
#ifdef COBJMACROS
|
||||
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \
|
||||
( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_AddRef(This) \
|
||||
( (This)->lpVtbl -> AddRef(This) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_Release(This) \
|
||||
( (This)->lpVtbl -> Release(This) )
|
||||
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginMakeCredential(This,request,response) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginMakeCredential(This,request,response) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginGetAssertion(This,request,response) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginGetAssertion(This,request,response) )
|
||||
|
||||
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginCancelOperation(This,request) \
|
||||
( (This)->lpVtbl -> EXPERIMENTAL_PluginCancelOperation(This,request) )
|
||||
|
||||
#endif /* COBJMACROS */
|
||||
|
||||
#endif /* C style interface */
|
||||
|
||||
#endif /* __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ */
|
||||
|
||||
/* Additional Prototypes for ALL interfaces */
|
||||
|
||||
unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
|
||||
void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * );
|
||||
|
||||
unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
|
||||
unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
|
||||
void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * );
|
||||
|
||||
/* end of Additional Prototypes */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
#![cfg(target_os = "windows")]
|
||||
|
||||
mod pa;
|
||||
|
||||
pub fn get_version_number() -> u64 {
|
||||
unsafe { pa::WebAuthNGetApiVersionNumber() }.into()
|
||||
}
|
||||
|
||||
pub fn add_authenticator() {
|
||||
unimplemented!();
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*
|
||||
The 'pa' (plugin authenticator) module will contain the generated
|
||||
bindgen code.
|
||||
|
||||
The attributes below will suppress warnings from the generated code.
|
||||
*/
|
||||
|
||||
#![cfg(target_os = "windows")]
|
||||
#![allow(clippy::all)]
|
||||
#![allow(warnings)]
|
||||
|
||||
include!(concat!(
|
||||
env!("OUT_DIR"),
|
||||
"/windows_pluginauthenticator_bindings.rs"
|
||||
));
|
||||
@@ -20,7 +20,7 @@
|
||||
"**/node_modules/@bitwarden/desktop-napi/index.js",
|
||||
"**/node_modules/@bitwarden/desktop-napi/desktop_napi.${platform}-${arch}*.node"
|
||||
],
|
||||
"electronVersion": "33.2.1",
|
||||
"electronVersion": "34.0.0",
|
||||
"generateUpdatesFilesForAllChannels": true,
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
@@ -133,7 +133,7 @@
|
||||
"entitlements": "resources/entitlements.mas.plist",
|
||||
"entitlementsInherit": "resources/entitlements.mas.inherit.plist",
|
||||
"entitlementsLoginHelper": "resources/entitlements.mas.loginhelper.plist",
|
||||
"hardenedRuntime": false,
|
||||
"hardenedRuntime": true,
|
||||
"extendInfo": {
|
||||
"LSMinimumSystemVersion": "12",
|
||||
"ElectronTeamID": "LTZ2PFU5D6"
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"rules": {
|
||||
"no-console": "off"
|
||||
}
|
||||
}
|
||||
@@ -12,15 +12,13 @@
|
||||
"@bitwarden/common": "file:../../../libs/common",
|
||||
"@bitwarden/node": "file:../../../libs/node",
|
||||
"module-alias": "2.2.3",
|
||||
"node-ipc": "9.2.1",
|
||||
"ts-node": "10.9.2",
|
||||
"uuid": "11.0.3",
|
||||
"uuid": "11.0.5",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.10.1",
|
||||
"@types/node-ipc": "9.2.3",
|
||||
"typescript": "4.7.4"
|
||||
"@types/node": "22.10.7",
|
||||
"typescript": "5.4.2"
|
||||
}
|
||||
},
|
||||
"../../../libs/common": {
|
||||
@@ -31,10 +29,7 @@
|
||||
"../../../libs/node": {
|
||||
"name": "@bitwarden/node",
|
||||
"version": "0.0.0",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bitwarden/common": "file:../common"
|
||||
}
|
||||
"license": "GPL-3.0"
|
||||
},
|
||||
"node_modules/@bitwarden/common": {
|
||||
"resolved": "../../../libs/common",
|
||||
@@ -106,24 +101,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz",
|
||||
"integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==",
|
||||
"version": "22.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz",
|
||||
"integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node-ipc": {
|
||||
"version": "9.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/node-ipc/-/node-ipc-9.2.3.tgz",
|
||||
"integrity": "sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
@@ -225,15 +210,6 @@
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/easy-stack": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/easy-stack/-/easy-stack-1.0.1.tgz",
|
||||
"integrity": "sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
@@ -249,15 +225,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/event-pubsub": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/event-pubsub/-/event-pubsub-4.3.0.tgz",
|
||||
"integrity": "sha512-z7IyloorXvKbFx9Bpie2+vMJKKx1fH1EN5yiTfp8CiLOTptSYy1g8H4yDpGlEdshL1PBiFtBHepF2cNsqeEeFQ==",
|
||||
"license": "Unlicense",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
@@ -276,27 +243,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/js-message": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
|
||||
"integrity": "sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-queue": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/js-queue/-/js-queue-2.0.2.tgz",
|
||||
"integrity": "sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"easy-stack": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
@@ -309,20 +255,6 @@
|
||||
"integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-ipc": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.2.1.tgz",
|
||||
"integrity": "sha512-mJzaM6O3xHf9VT8BULvJSbdVbmHUKRNOH7zDDkCrA1/T+CVjq2WVIDfLt0azZRXpgArJtl3rtmEozrbXPZ9GaQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"event-pubsub": "4.3.0",
|
||||
"js-message": "1.0.7",
|
||||
"js-queue": "2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -402,16 +334,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.7.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz",
|
||||
"integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==",
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
|
||||
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
@@ -421,9 +353,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.0.3",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
|
||||
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz",
|
||||
"integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
|
||||
@@ -17,15 +17,13 @@
|
||||
"@bitwarden/common": "file:../../../libs/common",
|
||||
"@bitwarden/node": "file:../../../libs/node",
|
||||
"module-alias": "2.2.3",
|
||||
"node-ipc": "9.2.1",
|
||||
"ts-node": "10.9.2",
|
||||
"uuid": "11.0.3",
|
||||
"uuid": "11.0.5",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.10.1",
|
||||
"@types/node-ipc": "9.2.3",
|
||||
"typescript": "4.7.4"
|
||||
"@types/node": "22.10.7",
|
||||
"typescript": "5.4.2"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
"@bitwarden/common": "dist/libs/common/src",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { hideBin } from "yargs/helpers";
|
||||
|
||||
import { NativeMessagingVersion } from "@bitwarden/common/enums";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CredentialCreatePayload } from "../../../src/models/native-messaging/encrypted-message-payloads/credential-create-payload";
|
||||
import { LogUtils } from "../log-utils";
|
||||
import NativeMessageService from "../native-message.service";
|
||||
|
||||
@@ -7,6 +7,7 @@ import { hideBin } from "yargs/helpers";
|
||||
|
||||
import { NativeMessagingVersion } from "@bitwarden/common/enums";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CredentialUpdatePayload } from "../../../src/models/native-messaging/encrypted-message-payloads/credential-update-payload";
|
||||
import { LogUtils } from "../log-utils";
|
||||
import NativeMessageService from "../native-message.service";
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { homedir } from "os";
|
||||
|
||||
import * as NodeIPC from "node-ipc";
|
||||
/* eslint-disable no-console */
|
||||
import { ChildProcess, spawn } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { MessageCommon } from "../../src/models/native-messaging/message-common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { UnencryptedMessageResponse } from "../../src/models/native-messaging/unencrypted-message-response";
|
||||
|
||||
import Deferred from "./deferred";
|
||||
import { race } from "./race";
|
||||
|
||||
NodeIPC.config.id = "native-messaging-test-runner";
|
||||
NodeIPC.config.maxRetries = 0;
|
||||
NodeIPC.config.silent = true;
|
||||
|
||||
const DESKTOP_APP_PATH = `${homedir}/tmp/app.bitwarden`;
|
||||
const DEFAULT_MESSAGE_TIMEOUT = 10 * 1000; // 10 seconds
|
||||
|
||||
export type MessageHandler = (MessageCommon) => void;
|
||||
@@ -39,6 +35,10 @@ export default class IPCService {
|
||||
// A set of deferred promises that are awaiting socket connection
|
||||
private awaitingConnection = new Set<Deferred<void>>();
|
||||
|
||||
// The IPC desktop_proxy process
|
||||
private process?: ChildProcess;
|
||||
private processOutputBuffer = Buffer.alloc(0);
|
||||
|
||||
constructor(
|
||||
private socketName: string,
|
||||
private messageHandler: MessageHandler,
|
||||
@@ -69,47 +69,47 @@ export default class IPCService {
|
||||
private _connect() {
|
||||
this.connectionState = IPCConnectionState.Connecting;
|
||||
|
||||
NodeIPC.connectTo(this.socketName, DESKTOP_APP_PATH, () => {
|
||||
// Process incoming message
|
||||
this.getSocket().on("message", (message: any) => {
|
||||
this.processMessage(message);
|
||||
});
|
||||
const proxyPath = selectProxyPath();
|
||||
console.log(`[IPCService] connecting to proxy at ${proxyPath}`);
|
||||
|
||||
this.getSocket().on("error", (error: Error) => {
|
||||
// Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be
|
||||
// invoked multiple times each time a connection error happens
|
||||
console.log("[IPCService] errored");
|
||||
console.log(
|
||||
"\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m",
|
||||
);
|
||||
this.awaitingConnection.forEach((deferred) => {
|
||||
console.log(`rejecting: ${deferred}`);
|
||||
deferred.reject(error);
|
||||
});
|
||||
this.awaitingConnection.clear();
|
||||
});
|
||||
this.process = spawn(proxyPath, process.argv.slice(1), {
|
||||
cwd: process.cwd(),
|
||||
stdio: "pipe",
|
||||
shell: false,
|
||||
});
|
||||
|
||||
this.getSocket().on("connect", () => {
|
||||
console.log("[IPCService] connected");
|
||||
this.connectionState = IPCConnectionState.Connected;
|
||||
this.process.stdout.on("data", (data: Buffer) => {
|
||||
this.processIpcMessage(data);
|
||||
});
|
||||
|
||||
this.awaitingConnection.forEach((deferred) => {
|
||||
deferred.resolve(null);
|
||||
});
|
||||
this.awaitingConnection.clear();
|
||||
});
|
||||
this.process.stderr.on("data", (data: Buffer) => {
|
||||
console.error(`proxy log: ${data}`);
|
||||
});
|
||||
|
||||
this.getSocket().on("disconnect", () => {
|
||||
console.log("[IPCService] disconnected");
|
||||
this.connectionState = IPCConnectionState.Disconnected;
|
||||
this.process.on("error", (error) => {
|
||||
// Only makes sense as long as config.maxRetries stays set to 0. Otherwise this will be
|
||||
// invoked multiple times each time a connection error happens
|
||||
console.log("[IPCService] errored");
|
||||
console.log(
|
||||
"\x1b[33m Please make sure the desktop app is running locally and 'Allow DuckDuckGo browser integration' setting is enabled \x1b[0m",
|
||||
);
|
||||
this.awaitingConnection.forEach((deferred) => {
|
||||
console.log(`rejecting: ${deferred}`);
|
||||
deferred.reject(error);
|
||||
});
|
||||
this.awaitingConnection.clear();
|
||||
});
|
||||
|
||||
this.process.on("exit", () => {
|
||||
console.log("[IPCService] disconnected");
|
||||
this.connectionState = IPCConnectionState.Disconnected;
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
console.log("[IPCService] disconnecting...");
|
||||
if (this.connectionState !== IPCConnectionState.Disconnected) {
|
||||
NodeIPC.disconnect(this.socketName);
|
||||
this.process?.kill();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ export default class IPCService {
|
||||
|
||||
this.pendingMessages.set(message.messageId, deferred);
|
||||
|
||||
this.getSocket().emit("message", message);
|
||||
this.sendIpcMessage(message);
|
||||
|
||||
try {
|
||||
// Since we can not guarantee that a response message will ever be sent, we put a timeout
|
||||
@@ -148,8 +148,56 @@ export default class IPCService {
|
||||
}
|
||||
}
|
||||
|
||||
private getSocket() {
|
||||
return NodeIPC.of[this.socketName];
|
||||
// As we're using the desktop_proxy to communicate with the native messaging directly,
|
||||
// the messages need to follow Native Messaging Host protocol (uint32 size followed by message).
|
||||
// https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging#native-messaging-host-protocol
|
||||
private sendIpcMessage(message: MessageCommon) {
|
||||
const messageStr = JSON.stringify(message);
|
||||
const buffer = Buffer.alloc(4 + messageStr.length);
|
||||
buffer.writeUInt32LE(messageStr.length, 0);
|
||||
buffer.write(messageStr, 4);
|
||||
|
||||
this.process?.stdin.write(buffer);
|
||||
}
|
||||
|
||||
private processIpcMessage(data: Buffer) {
|
||||
this.processOutputBuffer = Buffer.concat([this.processOutputBuffer, data]);
|
||||
|
||||
// We might receive more than one IPC message per data event, so we need to process them all
|
||||
// We continue as long as we have at least 4 + 1 bytes in the buffer, where the first 4 bytes
|
||||
// represent the message length and the 5th byte is the message
|
||||
while (this.processOutputBuffer.length > 4) {
|
||||
// Read the message length and ensure we have the full message
|
||||
const msgLength = this.processOutputBuffer.readUInt32LE(0);
|
||||
if (msgLength + 4 < this.processOutputBuffer.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the message from the buffer
|
||||
const messageStr = this.processOutputBuffer.subarray(4, msgLength + 4).toString();
|
||||
const message = JSON.parse(messageStr);
|
||||
|
||||
// Store the remaining buffer, which is part of the next message
|
||||
this.processOutputBuffer = this.processOutputBuffer.subarray(msgLength + 4);
|
||||
|
||||
// Process the connect/disconnect messages separately
|
||||
if (message?.command === "connected") {
|
||||
console.log("[IPCService] connected");
|
||||
this.connectionState = IPCConnectionState.Connected;
|
||||
|
||||
this.awaitingConnection.forEach((deferred) => {
|
||||
deferred.resolve(null);
|
||||
});
|
||||
this.awaitingConnection.clear();
|
||||
continue;
|
||||
} else if (message?.command === "disconnected") {
|
||||
console.log("[IPCService] disconnected");
|
||||
this.connectionState = IPCConnectionState.Disconnected;
|
||||
continue;
|
||||
}
|
||||
|
||||
this.processMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
private processMessage(message: any) {
|
||||
@@ -169,3 +217,41 @@ export default class IPCService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectProxyPath(): string {
|
||||
const proxyExtension = process.platform === "win32" ? ".exe" : "";
|
||||
|
||||
// If the PROXY_PATH environment variable is set, use that
|
||||
if (process.env.PROXY_PATH) {
|
||||
if (!fs.existsSync(process.env.PROXY_PATH)) {
|
||||
throw new Error(`PROXY_PATH is set to ${process.env.PROXY_PATH} but the file does not exist`);
|
||||
}
|
||||
return process.env.PROXY_PATH;
|
||||
}
|
||||
|
||||
// Otherwise try the debug build if present
|
||||
const debugProxyPath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"desktop_native",
|
||||
"target",
|
||||
"debug",
|
||||
`desktop_proxy${proxyExtension}`,
|
||||
);
|
||||
if (fs.existsSync(debugProxyPath)) {
|
||||
return debugProxyPath;
|
||||
}
|
||||
|
||||
// On MacOS, try the release build (sandboxed)
|
||||
const macReleaseProxyPath = `/Applications/Bitwarden.app/Contents/MacOS/desktop_proxy${proxyExtension}`;
|
||||
if (process.platform === "darwin" && fs.existsSync(macReleaseProxyPath)) {
|
||||
return macReleaseProxyPath;
|
||||
}
|
||||
|
||||
throw new Error("Could not find the desktop_proxy executable");
|
||||
}
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
/* eslint-disable no-console */
|
||||
import "module-alias/register";
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/key-management/crypto/services/encrypt.service.implementation";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ConsoleLogService } from "@bitwarden/common/platform/services/console-log.service";
|
||||
import { EncryptServiceImplementation } from "@bitwarden/common/platform/services/cryptography/encrypt.service.implementation";
|
||||
import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { DecryptedCommandData } from "../../src/models/native-messaging/decrypted-command-data";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EncryptedMessage } from "../../src/models/native-messaging/encrypted-message";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CredentialCreatePayload } from "../../src/models/native-messaging/encrypted-message-payloads/credential-create-payload";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CredentialUpdatePayload } from "../../src/models/native-messaging/encrypted-message-payloads/credential-update-payload";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { EncryptedMessageResponse } from "../../src/models/native-messaging/encrypted-message-response";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { MessageCommon } from "../../src/models/native-messaging/message-common";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { UnencryptedMessage } from "../../src/models/native-messaging/unencrypted-message";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { UnencryptedMessageResponse } from "../../src/models/native-messaging/unencrypted-message-response";
|
||||
|
||||
import IPCService, { IPCOptions } from "./ipc.service";
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
"target": "es6",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"sourceMap": false,
|
||||
"declaration": false,
|
||||
"paths": {
|
||||
"@src/*": ["src/*"],
|
||||
"@bitwarden/node/*": ["../../../libs/node/src/*"],
|
||||
"@bitwarden/common/*": ["../../../libs/common/src/*"]
|
||||
"@bitwarden/admin-console/*": ["../../../libs/admin-console/src/*"],
|
||||
"@bitwarden/auth/*": ["../../../libs/auth/src/*"],
|
||||
"@bitwarden/common/*": ["../../../libs/common/src/*"],
|
||||
"@bitwarden/key-management": ["../../../libs/key-management/src/"],
|
||||
"@bitwarden/node/*": ["../../../libs/node/src/*"]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2024.12.2",
|
||||
"version": "2025.3.0",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
@@ -19,7 +19,7 @@
|
||||
"postinstall": "electron-rebuild",
|
||||
"start": "cross-env ELECTRON_IS_DEV=0 ELECTRON_NO_UPDATER=1 electron ./build",
|
||||
"build-native": "cd desktop_native && node build.js",
|
||||
"build": "concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"",
|
||||
"build": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" concurrently -n Main,Rend,Prel -c yellow,cyan \"npm run build:main\" \"npm run build:renderer\" \"npm run build:preload\"",
|
||||
"build:dev": "concurrently -n Main,Rend -c yellow,cyan \"npm run build:main:dev\" \"npm run build:renderer:dev\"",
|
||||
"build:preload": "cross-env NODE_ENV=production webpack --config webpack.preload.js",
|
||||
"build:preload:watch": "cross-env NODE_ENV=production webpack --config webpack.preload.js --watch",
|
||||
@@ -35,7 +35,7 @@
|
||||
"clean:dist": "rimraf ./dist",
|
||||
"pack:dir": "npm run clean:dist && electron-builder --dir -p never",
|
||||
"pack:lin:flatpak": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --repo=build/.repo build/.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ./build/.repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop",
|
||||
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && mksquashfs ./dist/tmp-snap/ $SNAP_FILE -noappend -comp lzo -no-fragments && rm -rf ./dist/tmp-snap/",
|
||||
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snapcraft pack ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/",
|
||||
"pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never",
|
||||
"pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never",
|
||||
"pack:mac:mas": "npm run clean:dist && electron-builder --mac mas --universal -p never",
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/* eslint-disable no-undef */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
module.exports = {
|
||||
plugins: [require("tailwindcss"), require("autoprefixer"), require("postcss-nested")],
|
||||
plugins: [
|
||||
require("postcss-import"),
|
||||
require("postcss-nested"),
|
||||
require("tailwindcss"),
|
||||
require("autoprefixer"),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -8,7 +8,6 @@ command: bitwarden.sh
|
||||
finish-args:
|
||||
- --share=ipc
|
||||
- --share=network
|
||||
- --socket=wayland
|
||||
- --socket=x11
|
||||
- --device=dri
|
||||
- --env=XDG_CURRENT_DESKTOP=Unity
|
||||
@@ -33,11 +32,14 @@ modules:
|
||||
- install bitwarden.sh /app/bin/bitwarden.sh
|
||||
sources:
|
||||
- type: dir
|
||||
only-arches: [x86_64]
|
||||
path: ../dist/linux-unpacked
|
||||
- type: dir
|
||||
only-arches: [aarch64]
|
||||
path: ../dist/linux-arm64-unpacked
|
||||
- type: script
|
||||
dest-filename: bitwarden.sh
|
||||
commands:
|
||||
- ulimit -c 0
|
||||
- export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID"
|
||||
- exec zypak-wrapper /app/bin/bitwarden-app --ozone-platform-hint=auto
|
||||
--enable-features=WaylandWindowDecorations "$@"
|
||||
- exec zypak-wrapper /app/bin/bitwarden-app "$@"
|
||||
|
||||
@@ -6,5 +6,7 @@
|
||||
<true/>
|
||||
<key>com.apple.security.inherit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -8,5 +8,7 @@
|
||||
<array>
|
||||
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||
</array>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -4,10 +4,6 @@
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
<!--
|
||||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
||||
<true/>
|
||||
|
||||
@@ -6,13 +6,11 @@
|
||||
<true />
|
||||
<key>com.apple.security.inherit</key>
|
||||
<true />
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true />
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true />
|
||||
<!--
|
||||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
||||
<true/>
|
||||
-->
|
||||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
||||
<true/>
|
||||
-->
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -4,18 +4,12 @@
|
||||
<dict>
|
||||
<key>com.apple.application-identifier</key>
|
||||
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>LTZ2PFU5D6</string>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true />
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>LTZ2PFU5D6.com.bitwarden.desktop</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true />
|
||||
<key>com.apple.security.files.user-selected.read-write</key>
|
||||
<true />
|
||||
<key>com.apple.security.device.usb</key>
|
||||
<true />
|
||||
<!--
|
||||
<key>com.apple.developer.authentication-services.autofill-credential-provider</key>
|
||||
<true/>
|
||||
@@ -34,5 +28,7 @@
|
||||
<string>/Library/Application Support/Microsoft Edge Canary/NativeMessagingHosts/</string>
|
||||
<string>/Library/Application Support/Vivaldi/NativeMessagingHosts/</string>
|
||||
</array>
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true />
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports, no-console */
|
||||
require("dotenv").config();
|
||||
const child_process = require("child_process");
|
||||
const path = require("path");
|
||||
@@ -42,7 +42,7 @@ async function run(context) {
|
||||
if (process.env.GITHUB_ACTIONS === "true") {
|
||||
if (is_mas) {
|
||||
id = is_mas_dev
|
||||
? "E7C9978F6FBCE0553429185C405E61F5380BE8EB"
|
||||
? "4B9662CAB74E8E4F4ECBDD9EDEF2543659D95E3C"
|
||||
: "3rd Party Mac Developer Application: Bitwarden Inc";
|
||||
} else {
|
||||
id = "Developer ID Application: 8bit Solutions LLC";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports, no-console */
|
||||
require("dotenv").config();
|
||||
const path = require("path");
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports, no-console */
|
||||
const child = require("child_process");
|
||||
const { exit } = require("process");
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const concurrently = require("concurrently");
|
||||
const rimraf = require("rimraf");
|
||||
|
||||
@@ -25,7 +25,7 @@ concurrently(
|
||||
},
|
||||
{
|
||||
name: "Elec",
|
||||
command: `npx wait-on ./build/main.js && npx electron --inspect=5858 ${args.join(
|
||||
command: `npx wait-on ./build/main.js && npx electron --no-sandbox --inspect=5858 ${args.join(
|
||||
" ",
|
||||
)} ./build --watch`,
|
||||
prefixColor: "green",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires, no-console */
|
||||
/* eslint-disable @typescript-eslint/no-require-imports, no-console */
|
||||
|
||||
exports.default = async function (configuration) {
|
||||
if (parseInt(process.env.ELECTRON_BUILDER_SIGN) === 1 && configuration.path.slice(-4) == ".exe") {
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
}}</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="form-group">
|
||||
<div class="form-group" *ngIf="(pinEnabled$ | async) || this.form.value.pin">
|
||||
<div class="checkbox">
|
||||
<label for="pin">
|
||||
<input id="pin" type="checkbox" formControlName="pin" />
|
||||
@@ -163,7 +163,11 @@
|
||||
formControlName="requirePasswordOnStart"
|
||||
(change)="updateRequirePasswordOnStart()"
|
||||
/>
|
||||
{{ "requirePasswordOnStart" | i18n }}
|
||||
@if (pinEnabled$ | async) {
|
||||
{{ "requirePasswordOnStart" | i18n }}
|
||||
} @else {
|
||||
{{ "requirePasswordWithoutPinOnStart" | i18n }}
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
<small class="help-block form-group-child" *ngIf="isWindows">{{
|
||||
@@ -334,7 +338,7 @@
|
||||
</div>
|
||||
<small id="startToTrayHelp" class="help-block">{{ startToTrayDescText }}</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group" *ngIf="showOpenAtLoginOption">
|
||||
<div class="checkbox">
|
||||
<label for="openAtLogin">
|
||||
<input
|
||||
@@ -436,6 +440,23 @@
|
||||
"enableSshAgentDesc" | i18n
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="!isLinux">
|
||||
<div class="checkbox">
|
||||
<label for="allowScreenshots">
|
||||
<input
|
||||
id="allowScreenshots"
|
||||
type="checkbox"
|
||||
aria-describedby="allowScreenshotsHelp"
|
||||
formControlName="allowScreenshots"
|
||||
(change)="savePreventScreenshots()"
|
||||
/>
|
||||
{{ "allowScreenshots" | i18n }}
|
||||
</label>
|
||||
</div>
|
||||
<small id="allowScreenshotsHelp" class="help-block">{{
|
||||
"allowScreenshotsDesc" | i18n
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="form-group" *ngIf="showDuckDuckGoIntegrationOption">
|
||||
<div class="checkbox">
|
||||
<label for="enableDuckDuckGoBrowserIntegration">
|
||||
|
||||
324
apps/desktop/src/app/accounts/settings.component.spec.ts
Normal file
324
apps/desktop/src/app/accounts/settings.component.spec.ts
Normal file
@@ -0,0 +1,324 @@
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import {
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutStringType,
|
||||
VaultTimeoutAction,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { BiometricStateService, BiometricsStatus, KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service";
|
||||
|
||||
import { SettingsComponent } from "./settings.component";
|
||||
|
||||
describe("SettingsComponent", () => {
|
||||
let component: SettingsComponent;
|
||||
let fixture: ComponentFixture<SettingsComponent>;
|
||||
let originalIpc: any;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
||||
const biometricStateService = mock<BiometricStateService>();
|
||||
const policyService = mock<PolicyService>();
|
||||
const i18nService = mock<I18nService>();
|
||||
const autofillSettingsServiceAbstraction = mock<AutofillSettingsServiceAbstraction>();
|
||||
const desktopSettingsService = mock<DesktopSettingsService>();
|
||||
const domainSettingsService = mock<DomainSettingsService>();
|
||||
const desktopAutofillSettingsService = mock<DesktopAutofillSettingsService>();
|
||||
const themeStateService = mock<ThemeStateService>();
|
||||
const pinServiceAbstraction = mock<PinServiceAbstraction>();
|
||||
const desktopBiometricsService = mock<DesktopBiometricsService>();
|
||||
const platformUtilsService = mock<PlatformUtilsService>();
|
||||
|
||||
beforeEach(async () => {
|
||||
originalIpc = (global as any).ipc;
|
||||
(global as any).ipc = {
|
||||
auth: {
|
||||
loginRequest: jest.fn(),
|
||||
},
|
||||
platform: {
|
||||
isDev: false,
|
||||
isWindowsStore: false,
|
||||
powermonitor: {
|
||||
isLockMonitorAvailable: async () => false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
i18nService.supportedTranslationLocales = [];
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [SettingsComponent, I18nPipe],
|
||||
providers: [
|
||||
{
|
||||
provide: AutofillSettingsServiceAbstraction,
|
||||
useValue: autofillSettingsServiceAbstraction,
|
||||
},
|
||||
{ provide: AccountService, useValue: accountService },
|
||||
{ provide: BiometricStateService, useValue: biometricStateService },
|
||||
{ provide: ConfigService, useValue: mock<ConfigService>() },
|
||||
{
|
||||
provide: DesktopAutofillSettingsService,
|
||||
useValue: desktopAutofillSettingsService,
|
||||
},
|
||||
{ provide: DesktopBiometricsService, useValue: desktopBiometricsService },
|
||||
{ provide: DesktopSettingsService, useValue: desktopSettingsService },
|
||||
{ provide: DomainSettingsService, useValue: domainSettingsService },
|
||||
{ provide: DialogService, useValue: mock<DialogService>() },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
{ provide: LogService, useValue: mock<LogService>() },
|
||||
{ provide: MessageSender, useValue: mock<MessageSender>() },
|
||||
{
|
||||
provide: NativeMessagingManifestService,
|
||||
useValue: mock<NativeMessagingManifestService>(),
|
||||
},
|
||||
{ provide: KeyService, useValue: mock<KeyService>() },
|
||||
{ provide: PinServiceAbstraction, useValue: pinServiceAbstraction },
|
||||
{ provide: PlatformUtilsService, useValue: platformUtilsService },
|
||||
{ provide: PolicyService, useValue: policyService },
|
||||
{ provide: StateService, useValue: mock<StateService>() },
|
||||
{ provide: ThemeStateService, useValue: themeStateService },
|
||||
{ provide: UserVerificationService, useValue: mock<UserVerificationService>() },
|
||||
{ provide: VaultTimeoutSettingsService, useValue: vaultTimeoutSettingsService },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(SettingsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
||||
vaultTimeoutSettingsService.getVaultTimeoutByUserId$.mockReturnValue(
|
||||
of(VaultTimeoutStringType.OnLocked),
|
||||
);
|
||||
vaultTimeoutSettingsService.getVaultTimeoutActionByUserId$.mockReturnValue(
|
||||
of(VaultTimeoutAction.Lock),
|
||||
);
|
||||
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(false);
|
||||
biometricStateService.promptAutomatically$ = of(false);
|
||||
biometricStateService.requirePasswordOnStart$ = of(false);
|
||||
autofillSettingsServiceAbstraction.clearClipboardDelay$ = of(null);
|
||||
desktopSettingsService.minimizeOnCopy$ = of(false);
|
||||
desktopSettingsService.trayEnabled$ = of(false);
|
||||
desktopSettingsService.minimizeToTray$ = of(false);
|
||||
desktopSettingsService.closeToTray$ = of(false);
|
||||
desktopSettingsService.startToTray$ = of(false);
|
||||
desktopSettingsService.openAtLogin$ = of(false);
|
||||
desktopSettingsService.alwaysShowDock$ = of(false);
|
||||
desktopSettingsService.browserIntegrationEnabled$ = of(false);
|
||||
desktopSettingsService.browserIntegrationFingerprintEnabled$ = of(false);
|
||||
desktopSettingsService.hardwareAcceleration$ = of(false);
|
||||
desktopSettingsService.sshAgentEnabled$ = of(false);
|
||||
desktopSettingsService.preventScreenshots$ = of(false);
|
||||
domainSettingsService.showFavicons$ = of(false);
|
||||
desktopAutofillSettingsService.enableDuckDuckGoBrowserIntegration$ = of(false);
|
||||
themeStateService.selectedTheme$ = of(ThemeType.System);
|
||||
i18nService.userSetLocale$ = of("en");
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(global as any).ipc = originalIpc;
|
||||
});
|
||||
|
||||
it("pin enabled when RemoveUnlockWithPin policy is not set", async () => {
|
||||
// @ts-strict-ignore
|
||||
policyService.get$.mockReturnValue(of(null));
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
await expect(firstValueFrom(component.pinEnabled$)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("pin enabled when RemoveUnlockWithPin policy is disabled", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = false;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
await expect(firstValueFrom(component.pinEnabled$)).resolves.toBe(true);
|
||||
});
|
||||
|
||||
it("pin disabled when RemoveUnlockWithPin policy is enabled", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = true;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
|
||||
await component.ngOnInit();
|
||||
|
||||
await expect(firstValueFrom(component.pinEnabled$)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("pin visible when RemoveUnlockWithPin policy is not set", async () => {
|
||||
// @ts-strict-ignore
|
||||
policyService.get$.mockReturnValue(of(null));
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const pinInputElement = fixture.debugElement.query(By.css("#pin"));
|
||||
expect(pinInputElement).not.toBeNull();
|
||||
expect(pinInputElement.name).toBe("input");
|
||||
expect(pinInputElement.attributes).toMatchObject({
|
||||
type: "checkbox",
|
||||
});
|
||||
});
|
||||
|
||||
it("pin visible when RemoveUnlockWithPin policy is disabled", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = false;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const pinInputElement = fixture.debugElement.query(By.css("#pin"));
|
||||
expect(pinInputElement).not.toBeNull();
|
||||
expect(pinInputElement.name).toBe("input");
|
||||
expect(pinInputElement.attributes).toMatchObject({
|
||||
type: "checkbox",
|
||||
});
|
||||
});
|
||||
|
||||
it("pin visible when RemoveUnlockWithPin policy is enabled and pin set", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = true;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const pinInputElement = fixture.debugElement.query(By.css("#pin"));
|
||||
expect(pinInputElement).not.toBeNull();
|
||||
expect(pinInputElement.name).toBe("input");
|
||||
expect(pinInputElement.attributes).toMatchObject({
|
||||
type: "checkbox",
|
||||
});
|
||||
});
|
||||
|
||||
it("pin not visible when RemoveUnlockWithPin policy is enabled", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = true;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const pinInputElement = fixture.debugElement.query(By.css("#pin"));
|
||||
expect(pinInputElement).toBeNull();
|
||||
});
|
||||
|
||||
describe("biometrics enabled", () => {
|
||||
beforeEach(() => {
|
||||
desktopBiometricsService.getBiometricsStatus.mockResolvedValue(BiometricsStatus.Available);
|
||||
vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(true);
|
||||
});
|
||||
|
||||
it("require password or pin on app start message when RemoveUnlockWithPin policy is disabled and pin set and windows desktop", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = false;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
|
||||
i18nService.t.mockImplementation((id: string) => {
|
||||
if (id === "requirePasswordOnStart") {
|
||||
return "Require password or pin on app start";
|
||||
} else if (id === "requirePasswordWithoutPinOnStart") {
|
||||
return "Require password on app start";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const requirePasswordOnStartLabelElement = fixture.debugElement.query(
|
||||
By.css("label[for='requirePasswordOnStart']"),
|
||||
);
|
||||
expect(requirePasswordOnStartLabelElement).not.toBeNull();
|
||||
expect(requirePasswordOnStartLabelElement.children).toHaveLength(1);
|
||||
expect(requirePasswordOnStartLabelElement.children[0].name).toBe("input");
|
||||
expect(requirePasswordOnStartLabelElement.children[0].attributes).toMatchObject({
|
||||
id: "requirePasswordOnStart",
|
||||
type: "checkbox",
|
||||
});
|
||||
const textNodes = requirePasswordOnStartLabelElement.childNodes
|
||||
.filter((node) => node.nativeNode.nodeType === Node.TEXT_NODE)
|
||||
.map((node) => node.nativeNode.wholeText?.trim());
|
||||
expect(textNodes).toContain("Require password or pin on app start");
|
||||
});
|
||||
|
||||
it("require password on app start message when RemoveUnlockWithPin policy is enabled and pin set and windows desktop", async () => {
|
||||
const policy = new Policy();
|
||||
policy.type = PolicyType.RemoveUnlockWithPin;
|
||||
policy.enabled = true;
|
||||
policyService.get$.mockReturnValue(of(policy));
|
||||
platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop);
|
||||
i18nService.t.mockImplementation((id: string) => {
|
||||
if (id === "requirePasswordOnStart") {
|
||||
return "Require password or pin on app start";
|
||||
} else if (id === "requirePasswordWithoutPinOnStart") {
|
||||
return "Require password on app start";
|
||||
}
|
||||
return "";
|
||||
});
|
||||
pinServiceAbstraction.isPinSet.mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
|
||||
const requirePasswordOnStartLabelElement = fixture.debugElement.query(
|
||||
By.css("label[for='requirePasswordOnStart']"),
|
||||
);
|
||||
expect(requirePasswordOnStartLabelElement).not.toBeNull();
|
||||
expect(requirePasswordOnStartLabelElement.children).toHaveLength(1);
|
||||
expect(requirePasswordOnStartLabelElement.children[0].name).toBe("input");
|
||||
expect(requirePasswordOnStartLabelElement.children[0].attributes).toMatchObject({
|
||||
id: "requirePasswordOnStart",
|
||||
type: "checkbox",
|
||||
});
|
||||
const textNodes = requirePasswordOnStartLabelElement.childNodes
|
||||
.filter((node) => node.nativeNode.nodeType === Node.TEXT_NODE)
|
||||
.map((node) => node.nativeNode.wholeText?.trim());
|
||||
expect(textNodes).toContain("Require password on app start");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,19 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { BehaviorSubject, Observable, Subject, firstValueFrom } from "rxjs";
|
||||
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
|
||||
import { BehaviorSubject, Observable, Subject, firstValueFrom, of } from "rxjs";
|
||||
import {
|
||||
concatMap,
|
||||
debounceTime,
|
||||
filter,
|
||||
map,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
timeout,
|
||||
} from "rxjs/operators";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -15,27 +23,29 @@ import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/s
|
||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { KeySuffixOptions, ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { Theme, ThemeTypes } from "@bitwarden/common/platform/enums/theme-type.enum";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutOption,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService, BiometricsService, BiometricStateService } from "@bitwarden/key-management";
|
||||
import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management";
|
||||
|
||||
import { SetPinComponent } from "../../auth/components/set-pin.component";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
import { NativeMessagingManifestService } from "../services/native-messaging-manifest.service";
|
||||
|
||||
@@ -43,7 +53,6 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man
|
||||
selector: "app-settings",
|
||||
templateUrl: "settings.component.html",
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class SettingsComponent implements OnInit, OnDestroy {
|
||||
// For use in template
|
||||
protected readonly VaultTimeoutAction = VaultTimeoutAction;
|
||||
@@ -54,10 +63,12 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
themeOptions: any[];
|
||||
clearClipboardOptions: any[];
|
||||
supportsBiometric: boolean;
|
||||
private timerId: any;
|
||||
showAlwaysShowDock = false;
|
||||
requireEnableTray = false;
|
||||
showDuckDuckGoIntegrationOption = false;
|
||||
showSshAgentOption = false;
|
||||
showOpenAtLoginOption = false;
|
||||
isWindows: boolean;
|
||||
isLinux: boolean;
|
||||
|
||||
@@ -87,6 +98,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
userHasMasterPassword: boolean;
|
||||
userHasPinSet: boolean;
|
||||
|
||||
pinEnabled$: Observable<boolean> = of(true);
|
||||
|
||||
form = this.formBuilder.group({
|
||||
// Security
|
||||
vaultTimeout: [null as VaultTimeout | null],
|
||||
@@ -113,8 +126,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
}),
|
||||
enableHardwareAcceleration: true,
|
||||
enableSshAgent: false,
|
||||
allowScreenshots: false,
|
||||
enableDuckDuckGoBrowserIntegration: false,
|
||||
theme: [null as ThemeType | null],
|
||||
theme: [null as Theme | null],
|
||||
locale: [null as string | null],
|
||||
});
|
||||
|
||||
@@ -138,7 +152,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
private userVerificationService: UserVerificationServiceAbstraction,
|
||||
private desktopSettingsService: DesktopSettingsService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private biometricsService: BiometricsService,
|
||||
private biometricsService: DesktopBiometricsService,
|
||||
private desktopAutofillSettingsService: DesktopAutofillSettingsService,
|
||||
private pinService: PinServiceAbstraction,
|
||||
private logService: LogService,
|
||||
@@ -166,6 +180,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
this.startToTrayText = this.i18nService.t(startToTrayKey);
|
||||
this.startToTrayDescText = this.i18nService.t(startToTrayKey + "Desc");
|
||||
|
||||
this.showOpenAtLoginOption = !ipc.platform.isWindowsStore;
|
||||
|
||||
// DuckDuckGo browser is only for macos initially
|
||||
this.showDuckDuckGoIntegrationOption = isMac;
|
||||
|
||||
@@ -182,10 +198,9 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
this.localeOptions = localeOptions;
|
||||
|
||||
this.themeOptions = [
|
||||
{ name: this.i18nService.t("default"), value: ThemeType.System },
|
||||
{ name: this.i18nService.t("light"), value: ThemeType.Light },
|
||||
{ name: this.i18nService.t("dark"), value: ThemeType.Dark },
|
||||
{ name: "Nord", value: ThemeType.Nord },
|
||||
{ name: this.i18nService.t("default"), value: ThemeTypes.System },
|
||||
{ name: this.i18nService.t("light"), value: ThemeTypes.Light },
|
||||
{ name: this.i18nService.t("dark"), value: ThemeTypes.Dark },
|
||||
];
|
||||
|
||||
this.clearClipboardOptions = [
|
||||
@@ -247,6 +262,12 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
// Load initial values
|
||||
this.userHasPinSet = await this.pinService.isPinSet(activeAccount.id);
|
||||
|
||||
this.pinEnabled$ = this.policyService.get$(PolicyType.RemoveUnlockWithPin).pipe(
|
||||
map((policy) => {
|
||||
return policy == null || !policy.enabled;
|
||||
}),
|
||||
);
|
||||
|
||||
const initialValues = {
|
||||
vaultTimeout: await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.getVaultTimeoutByUserId$(activeAccount.id),
|
||||
@@ -282,6 +303,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
this.desktopSettingsService.hardwareAcceleration$,
|
||||
),
|
||||
enableSshAgent: await firstValueFrom(this.desktopSettingsService.sshAgentEnabled$),
|
||||
allowScreenshots: !(await firstValueFrom(this.desktopSettingsService.preventScreenshots$)),
|
||||
theme: await firstValueFrom(this.themeStateService.selectedTheme$),
|
||||
locale: await firstValueFrom(this.i18nService.userSetLocale$),
|
||||
};
|
||||
@@ -294,7 +316,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
// Non-form values
|
||||
this.showMinToTray = this.platformUtilsService.getDevice() !== DeviceType.LinuxDesktop;
|
||||
this.showAlwaysShowDock = this.platformUtilsService.getDevice() === DeviceType.MacOsDesktop;
|
||||
this.supportsBiometric = await this.biometricsService.supportsBiometric();
|
||||
this.previousVaultTimeout = this.form.value.vaultTimeout;
|
||||
|
||||
this.refreshTimeoutSettings$
|
||||
@@ -357,6 +378,23 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
this.form.controls.enableBrowserIntegrationFingerprint.disable();
|
||||
}
|
||||
});
|
||||
|
||||
this.supportsBiometric = this.shouldAllowBiometricSetup(
|
||||
await this.biometricsService.getBiometricsStatus(),
|
||||
);
|
||||
this.timerId = setInterval(async () => {
|
||||
this.supportsBiometric = this.shouldAllowBiometricSetup(
|
||||
await this.biometricsService.getBiometricsStatus(),
|
||||
);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private shouldAllowBiometricSetup(biometricStatus: BiometricsStatus): boolean {
|
||||
return [
|
||||
BiometricsStatus.Available,
|
||||
BiometricsStatus.AutoSetupNeeded,
|
||||
BiometricsStatus.ManualSetupNeeded,
|
||||
].includes(biometricStatus);
|
||||
}
|
||||
|
||||
async saveVaultTimeout(newValue: VaultTimeout) {
|
||||
@@ -473,23 +511,20 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
const needsSetup = await this.biometricsService.biometricsNeedsSetup();
|
||||
const supportsBiometricAutoSetup = await this.biometricsService.biometricsSupportsAutoSetup();
|
||||
const status = await this.biometricsService.getBiometricsStatus();
|
||||
|
||||
if (needsSetup) {
|
||||
if (supportsBiometricAutoSetup) {
|
||||
await this.biometricsService.biometricsSetup();
|
||||
} else {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "biometricsManualSetupTitle" },
|
||||
content: { key: "biometricsManualSetupDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
if (confirmed) {
|
||||
this.platformUtilsService.launchUri("https://bitwarden.com/help/biometrics/");
|
||||
}
|
||||
return;
|
||||
if (status === BiometricsStatus.AutoSetupNeeded) {
|
||||
await this.biometricsService.setupBiometrics();
|
||||
} else if (status === BiometricsStatus.ManualSetupNeeded) {
|
||||
const confirmed = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "biometricsManualSetupTitle" },
|
||||
content: { key: "biometricsManualSetupDesc" },
|
||||
type: "warning",
|
||||
});
|
||||
if (confirmed) {
|
||||
this.platformUtilsService.launchUri("https://bitwarden.com/help/biometrics/");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await this.biometricStateService.setBiometricUnlockEnabled(true);
|
||||
@@ -510,8 +545,13 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
await this.keyService.refreshAdditionalKeys();
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
// Validate the key is stored in case biometrics fail.
|
||||
const biometricSet = await this.keyService.hasUserKeyStored(KeySuffixOptions.Biometric);
|
||||
const biometricSet =
|
||||
(await this.biometricsService.getBiometricsStatusForUser(activeUserId)) ===
|
||||
BiometricsStatus.Available;
|
||||
this.form.controls.biometric.setValue(biometricSet, { emitEvent: false });
|
||||
if (!biometricSet) {
|
||||
await this.biometricStateService.setBiometricUnlockEnabled(false);
|
||||
@@ -637,7 +677,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
const skipSupportedPlatformCheck =
|
||||
ipc.platform.allowBrowserintegrationOverride || ipc.platform.isDev;
|
||||
|
||||
if (skipSupportedPlatformCheck) {
|
||||
if (!skipSupportedPlatformCheck) {
|
||||
if (
|
||||
ipc.platform.deviceType === DeviceType.MacOsDesktop &&
|
||||
!this.platformUtilsService.isMacAppStore()
|
||||
@@ -746,6 +786,33 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
await this.desktopSettingsService.setSshAgentEnabled(this.form.value.enableSshAgent);
|
||||
}
|
||||
|
||||
async savePreventScreenshots() {
|
||||
await this.desktopSettingsService.setPreventScreenshots(!this.form.value.allowScreenshots);
|
||||
|
||||
if (!this.form.value.allowScreenshots) {
|
||||
const dialogRef = this.dialogService.openSimpleDialogRef({
|
||||
title: { key: "confirmWindowStillVisibleTitle" },
|
||||
content: { key: "confirmWindowStillVisibleContent" },
|
||||
acceptButtonText: { key: "ok" },
|
||||
cancelButtonText: null,
|
||||
type: "info",
|
||||
});
|
||||
let enabled = true;
|
||||
try {
|
||||
enabled = await firstValueFrom(dialogRef.closed.pipe(timeout(10000)));
|
||||
} catch {
|
||||
enabled = false;
|
||||
} finally {
|
||||
dialogRef.close();
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
await this.desktopSettingsService.setPreventScreenshots(false);
|
||||
this.form.controls.allowScreenshots.setValue(true, { emitEvent: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async generateVaultTimeoutOptions(): Promise<VaultTimeoutOption[]> {
|
||||
let vaultTimeoutOptions: VaultTimeoutOption[] = [
|
||||
{ name: this.i18nService.t("oneMinute"), value: 1 },
|
||||
@@ -776,6 +843,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
clearInterval(this.timerId);
|
||||
}
|
||||
|
||||
get biometricText() {
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
import { NgModule } from "@angular/core";
|
||||
import { RouterModule, Routes } from "@angular/router";
|
||||
|
||||
import { AuthenticationTimeoutComponent } from "@bitwarden/angular/auth/components/authentication-timeout.component";
|
||||
import {
|
||||
DesktopDefaultOverlayPosition,
|
||||
EnvironmentSelectorComponent,
|
||||
} from "@bitwarden/angular/auth/components/environment-selector.component";
|
||||
import { TwoFactorTimeoutComponent } from "@bitwarden/angular/auth/components/two-factor-auth/two-factor-auth-expired.component";
|
||||
import { unauthUiRefreshSwap } from "@bitwarden/angular/auth/functions/unauth-ui-refresh-route-swap";
|
||||
import {
|
||||
authGuard,
|
||||
lockGuard,
|
||||
activeAuthGuard,
|
||||
redirectGuard,
|
||||
tdeDecryptionRequiredGuard,
|
||||
unauthGuardFn,
|
||||
} from "@bitwarden/angular/auth/guards";
|
||||
import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard";
|
||||
import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect";
|
||||
import { NewDeviceVerificationNoticeGuard } from "@bitwarden/angular/vault/guards";
|
||||
import {
|
||||
AnonLayoutWrapperComponent,
|
||||
@@ -23,7 +22,6 @@ import {
|
||||
LoginComponent,
|
||||
LoginSecondaryContentComponent,
|
||||
LockIcon,
|
||||
LockV2Component,
|
||||
LoginViaAuthRequestComponent,
|
||||
PasswordHintComponent,
|
||||
RegistrationFinishComponent,
|
||||
@@ -39,28 +37,23 @@ import {
|
||||
DevicesIcon,
|
||||
SsoComponent,
|
||||
TwoFactorTimeoutIcon,
|
||||
TwoFactorAuthComponent,
|
||||
TwoFactorAuthGuard,
|
||||
NewDeviceVerificationComponent,
|
||||
DeviceVerificationIcon,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { LockComponent } from "@bitwarden/key-management-ui";
|
||||
import {
|
||||
NewDeviceVerificationNoticePageOneComponent,
|
||||
NewDeviceVerificationNoticePageTwoComponent,
|
||||
VaultIcons,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { twofactorRefactorSwap } from "../../../../libs/angular/src/utils/two-factor-component-refactor-route-swap";
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
|
||||
import { HintComponent } from "../auth/hint.component";
|
||||
import { LockComponent } from "../auth/lock.component";
|
||||
import { LoginDecryptionOptionsComponentV1 } from "../auth/login/login-decryption-options/login-decryption-options-v1.component";
|
||||
import { LoginComponentV1 } from "../auth/login/login-v1.component";
|
||||
import { LoginViaAuthRequestComponentV1 } from "../auth/login/login-via-auth-request-v1.component";
|
||||
import { RegisterComponent } from "../auth/register.component";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { SsoComponentV1 } from "../auth/sso-v1.component";
|
||||
import { TwoFactorAuthComponent } from "../auth/two-factor-auth.component";
|
||||
import { TwoFactorComponent } from "../auth/two-factor.component";
|
||||
import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { VaultComponent } from "../vault/app/vault/vault.component";
|
||||
|
||||
@@ -70,6 +63,7 @@ import { SendComponent } from "./tools/send/send.component";
|
||||
/**
|
||||
* Data properties acceptable for use in route objects in the desktop
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
export interface RouteDataProperties {
|
||||
// For any new route data properties, add them here.
|
||||
// then assert that the data object satisfies this interface in the route object.
|
||||
@@ -82,37 +76,35 @@ const routes: Routes = [
|
||||
children: [], // Children lets us have an empty component.
|
||||
canActivate: [redirectGuard({ loggedIn: "/vault", loggedOut: "/login", locked: "/lock" })],
|
||||
},
|
||||
{
|
||||
path: "lock",
|
||||
component: LockComponent,
|
||||
canActivate: [lockGuard()],
|
||||
canMatch: [extensionRefreshRedirect("/lockV2")],
|
||||
},
|
||||
...twofactorRefactorSwap(
|
||||
TwoFactorComponent,
|
||||
...unauthUiRefreshSwap(
|
||||
TwoFactorComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "2fa",
|
||||
},
|
||||
{
|
||||
path: "2fa",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
canActivate: [unauthGuardFn(), TwoFactorAuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: TwoFactorAuthComponent,
|
||||
canActivate: [unauthGuardFn()],
|
||||
},
|
||||
],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "2fa-timeout",
|
||||
path: "authentication-timeout",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: TwoFactorTimeoutComponent,
|
||||
component: AuthenticationTimeoutComponent,
|
||||
},
|
||||
],
|
||||
data: {
|
||||
@@ -122,7 +114,21 @@ const routes: Routes = [
|
||||
},
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{ path: "register", component: RegisterComponent },
|
||||
{
|
||||
path: "device-verification",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
canActivate: [unauthGuardFn(), activeAuthGuard()],
|
||||
children: [{ path: "", component: NewDeviceVerificationComponent }],
|
||||
data: {
|
||||
pageIcon: DeviceVerificationIcon,
|
||||
pageTitle: {
|
||||
key: "verifyYourIdentity",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "weDontRecognizeThisDevice",
|
||||
},
|
||||
} satisfies RouteDataProperties & AnonLayoutWrapperData,
|
||||
},
|
||||
{
|
||||
path: "new-device-notice",
|
||||
component: AnonLayoutWrapperComponent,
|
||||
@@ -157,33 +163,6 @@ const routes: Routes = [
|
||||
},
|
||||
{ path: "accessibility-cookie", component: AccessibilityCookieComponent },
|
||||
{ path: "set-password", component: SetPasswordComponent },
|
||||
...unauthUiRefreshSwap(
|
||||
SsoComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "sso",
|
||||
},
|
||||
{
|
||||
path: "sso",
|
||||
data: {
|
||||
pageIcon: VaultIcon,
|
||||
pageTitle: {
|
||||
key: "enterpriseSingleSignOn",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "singleSignOnEnterOrgIdentifierText",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: SsoComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "send",
|
||||
component: SendComponent,
|
||||
@@ -199,139 +178,10 @@ const routes: Routes = [
|
||||
component: RemovePasswordComponent,
|
||||
canActivate: [authGuard],
|
||||
},
|
||||
...unauthUiRefreshSwap(
|
||||
LoginViaAuthRequestComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login-with-device",
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "loginInitiated",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginViaAuthRequestComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginViaAuthRequestComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "adminApprovalRequested",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "adminApprovalRequestSentToAdmins",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [{ path: "", component: LoginViaAuthRequestComponent }],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
HintComponent,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn()],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "requestPasswordHint",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
|
||||
},
|
||||
pageIcon: UserLockIcon,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: PasswordHintComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login",
|
||||
component: LoginComponentV1,
|
||||
canActivate: [maxAccountsGuardFn()],
|
||||
},
|
||||
{
|
||||
path: "",
|
||||
children: [
|
||||
{
|
||||
path: "login",
|
||||
canActivate: [maxAccountsGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "logInToBitwarden",
|
||||
},
|
||||
pageIcon: VaultIcon,
|
||||
},
|
||||
children: [
|
||||
{ path: "", component: LoginComponent },
|
||||
{ path: "", component: LoginSecondaryContentComponent, outlet: "secondary" },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
...unauthUiRefreshSwap(
|
||||
LoginDecryptionOptionsComponentV1,
|
||||
AnonLayoutWrapperComponent,
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
},
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
},
|
||||
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||
},
|
||||
),
|
||||
{
|
||||
path: "passkeys",
|
||||
component: Fido2PlaceholderComponent,
|
||||
},
|
||||
{
|
||||
path: "passkeys",
|
||||
component: Fido2PlaceholderComponent,
|
||||
@@ -342,7 +192,7 @@ const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: "signup",
|
||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageIcon: RegistrationUserAddIcon,
|
||||
pageTitle: {
|
||||
@@ -366,7 +216,7 @@ const routes: Routes = [
|
||||
},
|
||||
{
|
||||
path: "finish-signup",
|
||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification), unauthGuardFn()],
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageIcon: RegistrationLockAltIcon,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
@@ -378,8 +228,112 @@ const routes: Routes = [
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "lockV2",
|
||||
canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()],
|
||||
path: "login",
|
||||
canActivate: [maxAccountsGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "logInToBitwarden",
|
||||
},
|
||||
pageIcon: VaultIcon,
|
||||
},
|
||||
children: [
|
||||
{ path: "", component: LoginComponent },
|
||||
{ path: "", component: LoginSecondaryContentComponent, outlet: "secondary" },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
data: {
|
||||
overlayPosition: DesktopDefaultOverlayPosition,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login-initiated",
|
||||
canActivate: [tdeDecryptionRequiredGuard()],
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
},
|
||||
children: [{ path: "", component: LoginDecryptionOptionsComponent }],
|
||||
},
|
||||
{
|
||||
path: "sso",
|
||||
data: {
|
||||
pageIcon: VaultIcon,
|
||||
pageTitle: {
|
||||
key: "enterpriseSingleSignOn",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "singleSignOnEnterOrgIdentifierText",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: SsoComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "login-with-device",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "logInRequestSent",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "aNotificationWasSentToYourDevice",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: LoginViaAuthRequestComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "admin-approval-requested",
|
||||
data: {
|
||||
pageIcon: DevicesIcon,
|
||||
pageTitle: {
|
||||
key: "adminApprovalRequested",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "adminApprovalRequestSentToAdmins",
|
||||
},
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [{ path: "", component: LoginViaAuthRequestComponent }],
|
||||
},
|
||||
{
|
||||
path: "hint",
|
||||
canActivate: [unauthGuardFn()],
|
||||
data: {
|
||||
pageTitle: {
|
||||
key: "requestPasswordHint",
|
||||
},
|
||||
pageSubtitle: {
|
||||
key: "enterYourAccountEmailAddressAndYourPasswordHintWillBeSentToYou",
|
||||
},
|
||||
pageIcon: UserLockIcon,
|
||||
} satisfies AnonLayoutWrapperData,
|
||||
children: [
|
||||
{ path: "", component: PasswordHintComponent },
|
||||
{
|
||||
path: "",
|
||||
component: EnvironmentSelectorComponent,
|
||||
outlet: "environment-selector",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "lock",
|
||||
canActivate: [lockGuard()],
|
||||
data: {
|
||||
pageIcon: LockIcon,
|
||||
pageTitle: {
|
||||
@@ -390,13 +344,12 @@ const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
component: LockV2Component,
|
||||
component: LockComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "set-password-jit",
|
||||
canActivate: [canAccessFeature(FeatureFlag.EmailVerification)],
|
||||
component: SetPasswordJitComponent,
|
||||
data: {
|
||||
pageTitle: {
|
||||
|
||||
@@ -12,28 +12,16 @@ import {
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { Router } from "@angular/router";
|
||||
import {
|
||||
catchError,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
of,
|
||||
Subject,
|
||||
takeUntil,
|
||||
timeout,
|
||||
withLatestFrom,
|
||||
} from "rxjs";
|
||||
import { filter, firstValueFrom, map, Subject, takeUntil, timeout, withLatestFrom } from "rxjs";
|
||||
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { DeviceTrustToastService } from "@bitwarden/angular/auth/services/device-trust-toast.service.abstraction";
|
||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import { FingerprintDialogComponent, LoginApprovalComponent } from "@bitwarden/auth/angular";
|
||||
import { LogoutReason } from "@bitwarden/auth/common";
|
||||
import { DESKTOP_SSO_CALLBACK, LogoutReason } from "@bitwarden/auth/common";
|
||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -43,43 +31,45 @@ import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstrac
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { VaultTimeoutAction } from "@bitwarden/common/enums/vault-timeout-action.enum";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import {
|
||||
VaultTimeout,
|
||||
VaultTimeoutAction,
|
||||
VaultTimeoutService,
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
import { clearCaches } from "@bitwarden/common/platform/misc/sequentialize";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { StateEventRunnerService } from "@bitwarden/common/platform/state";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { DialogService, ToastOptions, ToastService } from "@bitwarden/components";
|
||||
import { CredentialGeneratorHistoryDialogComponent } from "@bitwarden/generator-components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
import { KeyService, BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
import { MenuAccount, MenuUpdateRequest } from "../main/menu/menu.updater";
|
||||
import { flagEnabled } from "../platform/flags";
|
||||
import { PremiumComponent } from "../vault/app/accounts/premium.component";
|
||||
import { FolderAddEditComponent } from "../vault/app/vault/folder-add-edit.component";
|
||||
|
||||
import { SettingsComponent } from "./accounts/settings.component";
|
||||
import { ExportDesktopComponent } from "./tools/export/export-desktop.component";
|
||||
import { CredentialGeneratorComponent } from "./tools/generator/credential-generator.component";
|
||||
import { GeneratorComponent } from "./tools/generator.component";
|
||||
import { ImportDesktopComponent } from "./tools/import/import-desktop.component";
|
||||
import { PasswordGeneratorHistoryComponent } from "./tools/password-generator-history.component";
|
||||
|
||||
const BroadcasterSubscriptionId = "AppComponent";
|
||||
const IdleTimeout = 60000 * 10; // 10 minutes
|
||||
@@ -104,6 +94,8 @@ const SyncInterval = 6 * 60 * 60 * 1000; // 6 hours
|
||||
</div>
|
||||
<router-outlet *ngIf="!loading"></router-outlet>
|
||||
</div>
|
||||
|
||||
<bit-toast-container></bit-toast-container>
|
||||
`,
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
@@ -138,7 +130,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private broadcasterService: BroadcasterService,
|
||||
private folderService: InternalFolderService,
|
||||
private syncService: SyncService,
|
||||
private passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
private cipherService: CipherService,
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
@@ -167,27 +158,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private biometricStateService: BiometricStateService,
|
||||
private stateEventRunnerService: StateEventRunnerService,
|
||||
private accountService: AccountService,
|
||||
private sdkService: SdkService,
|
||||
private organizationService: OrganizationService,
|
||||
private deviceTrustToastService: DeviceTrustToastService,
|
||||
) {
|
||||
if (flagEnabled("sdk")) {
|
||||
// Warn if the SDK for some reason can't be initialized
|
||||
this.sdkService.supported$
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
catchError(() => {
|
||||
return of(false);
|
||||
}),
|
||||
)
|
||||
.subscribe((supported) => {
|
||||
if (!supported) {
|
||||
this.logService.debug("SDK is not supported");
|
||||
this.sdkService.failedToInitialize("desktop").catch((e) => this.logService.error(e));
|
||||
} else {
|
||||
this.logService.debug("SDK is supported");
|
||||
}
|
||||
});
|
||||
}
|
||||
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -224,17 +198,11 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.recordActivity();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.notificationsService.updateConnection();
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.updateAppMenu();
|
||||
this.processReloadService.cancelProcessReload();
|
||||
break;
|
||||
case "loggedOut":
|
||||
this.modalService.closeAll();
|
||||
if (message.userId == null || message.userId === this.activeUserId) {
|
||||
await this.notificationsService.updateConnection();
|
||||
}
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.updateAppMenu();
|
||||
@@ -278,9 +246,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
) {
|
||||
await this.router.navigate(["lock"]);
|
||||
}
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.notificationsService.updateConnection();
|
||||
await this.updateAppMenu();
|
||||
await this.systemService.clearPendingClipboard();
|
||||
await this.processReloadService.startProcessReload(this.authService);
|
||||
@@ -342,7 +307,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
const queryParams = {
|
||||
code: message.code,
|
||||
state: message.state,
|
||||
redirectUri: message.redirectUri ?? "bitwarden://sso-callback",
|
||||
redirectUri: message.redirectUri ?? DESKTOP_SSO_CALLBACK,
|
||||
};
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
@@ -535,41 +500,13 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async openGenerator() {
|
||||
const isGeneratorSwapEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.GeneratorToolsModernization,
|
||||
);
|
||||
if (isGeneratorSwapEnabled) {
|
||||
await this.dialogService.open(CredentialGeneratorComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
this.modalService.closeAll();
|
||||
|
||||
[this.modal] = await this.modalService.openViewRef(
|
||||
GeneratorComponent,
|
||||
this.generatorModalRef,
|
||||
(comp) => (comp.comingFromAddEdit = false),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.modal.onClosed.subscribe(() => {
|
||||
this.modal = null;
|
||||
});
|
||||
await this.dialogService.open(CredentialGeneratorComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
async openGeneratorHistory() {
|
||||
const isGeneratorSwapEnabled = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.GeneratorToolsModernization,
|
||||
);
|
||||
if (isGeneratorSwapEnabled) {
|
||||
await this.dialogService.open(CredentialGeneratorHistoryDialogComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.openModal<PasswordGeneratorHistoryComponent>(
|
||||
PasswordGeneratorHistoryComponent,
|
||||
this.passwordHistoryRef,
|
||||
);
|
||||
await this.dialogService.open(CredentialGeneratorHistoryDialogComponent);
|
||||
return;
|
||||
}
|
||||
|
||||
private async updateAppMenu() {
|
||||
@@ -787,12 +724,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
private idleStateChanged() {
|
||||
if (this.isIdle) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.notificationsService.disconnectFromInactivity();
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.notificationsService.reconnectFromActivity();
|
||||
}
|
||||
}
|
||||
@@ -883,7 +816,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (urlString.indexOf("bitwarden://import-callback-lp") === 0) {
|
||||
message = "importCallbackLastPass";
|
||||
} else if (urlString.indexOf("bitwarden://sso-callback") === 0) {
|
||||
} else if (urlString.indexOf(DESKTOP_SSO_CALLBACK) === 0) {
|
||||
message = "ssoCallback";
|
||||
}
|
||||
|
||||
@@ -891,9 +824,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async deleteAccount() {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
await firstValueFrom(
|
||||
this.configService.getFeatureFlag$(FeatureFlag.AccountDeprovisioning).pipe(
|
||||
withLatestFrom(this.organizationService.organizations$),
|
||||
withLatestFrom(this.organizationService.organizations$(userId)),
|
||||
map(async ([accountDeprovisioningEnabled, organization]) => {
|
||||
if (
|
||||
accountDeprovisioningEnabled &&
|
||||
|
||||
@@ -7,23 +7,22 @@ import { NgModule } from "@angular/core";
|
||||
|
||||
import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe";
|
||||
import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe";
|
||||
import { DialogModule, CalloutModule } from "@bitwarden/components";
|
||||
import { CalloutModule, DialogModule } from "@bitwarden/components";
|
||||
import { DecryptionFailureDialogComponent } from "@bitwarden/vault";
|
||||
|
||||
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
|
||||
import { DeleteAccountComponent } from "../auth/delete-account.component";
|
||||
import { EnvironmentComponent } from "../auth/environment.component";
|
||||
import { HintComponent } from "../auth/hint.component";
|
||||
import { LockComponent } from "../auth/lock.component";
|
||||
import { LoginModule } from "../auth/login/login.module";
|
||||
import { RegisterComponent } from "../auth/register.component";
|
||||
import { RemovePasswordComponent } from "../auth/remove-password.component";
|
||||
import { SetPasswordComponent } from "../auth/set-password.component";
|
||||
import { SsoComponentV1 } from "../auth/sso-v1.component";
|
||||
import { TwoFactorOptionsComponent } from "../auth/two-factor-options.component";
|
||||
import { TwoFactorComponent } from "../auth/two-factor.component";
|
||||
import { TwoFactorOptionsComponentV1 } from "../auth/two-factor-options-v1.component";
|
||||
import { TwoFactorComponentV1 } from "../auth/two-factor-v1.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
|
||||
import { SshAgentService } from "../platform/services/ssh-agent.service";
|
||||
import { PremiumComponent } from "../vault/app/accounts/premium.component";
|
||||
import { SshAgentService } from "../autofill/services/ssh-agent.service";
|
||||
import { PremiumComponent } from "../billing/app/accounts/premium.component";
|
||||
import { AddEditCustomFieldsComponent } from "../vault/app/vault/add-edit-custom-fields.component";
|
||||
import { AddEditComponent } from "../vault/app/vault/add-edit.component";
|
||||
import { AttachmentsComponent } from "../vault/app/vault/attachments.component";
|
||||
@@ -47,8 +46,6 @@ import { HeaderComponent } from "./layout/header.component";
|
||||
import { NavComponent } from "./layout/nav.component";
|
||||
import { SearchComponent } from "./layout/search/search.component";
|
||||
import { SharedModule } from "./shared/shared.module";
|
||||
import { GeneratorComponent } from "./tools/generator.component";
|
||||
import { PasswordGeneratorHistoryComponent } from "./tools/password-generator-history.component";
|
||||
import { AddEditComponent as SendAddEditComponent } from "./tools/send/add-edit.component";
|
||||
import { SendComponent } from "./tools/send/send.component";
|
||||
|
||||
@@ -62,6 +59,7 @@ import { SendComponent } from "./tools/send/send.component";
|
||||
CalloutModule,
|
||||
DeleteAccountComponent,
|
||||
UserVerificationComponent,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
declarations: [
|
||||
AccessibilityCookieComponent,
|
||||
@@ -78,13 +76,9 @@ import { SendComponent } from "./tools/send/send.component";
|
||||
FolderAddEditComponent,
|
||||
HeaderComponent,
|
||||
HintComponent,
|
||||
LockComponent,
|
||||
NavComponent,
|
||||
GeneratorComponent,
|
||||
PasswordGeneratorHistoryComponent,
|
||||
PasswordHistoryComponent,
|
||||
PremiumComponent,
|
||||
RegisterComponent,
|
||||
RemovePasswordComponent,
|
||||
SearchComponent,
|
||||
SendAddEditComponent,
|
||||
@@ -92,9 +86,9 @@ import { SendComponent } from "./tools/send/send.component";
|
||||
SetPasswordComponent,
|
||||
SettingsComponent,
|
||||
ShareComponent,
|
||||
TwoFactorComponentV1,
|
||||
SsoComponentV1,
|
||||
TwoFactorComponent,
|
||||
TwoFactorOptionsComponent,
|
||||
TwoFactorOptionsComponentV1,
|
||||
UpdateTempPasswordComponent,
|
||||
VaultComponent,
|
||||
VaultTimeoutInputComponent,
|
||||
|
||||
@@ -76,12 +76,10 @@
|
||||
></app-avatar>
|
||||
<div class="accountInfo">
|
||||
<span class="sr-only">{{ "switchAccount" | i18n }}: </span>
|
||||
<span class="email" aria-hidden="true">{{ account.value.email }}</span>
|
||||
<span class="server" aria-hidden="true">
|
||||
<span class="sr-only"> / </span>{{ account.value.server }}
|
||||
</span>
|
||||
<span class="status" aria-hidden="true"
|
||||
><span class="sr-only"> (</span
|
||||
<span class="email">{{ account.value.email }}</span>
|
||||
<span class="server"> <span class="sr-only"> / </span>{{ account.value.server }} </span>
|
||||
<span class="status">
|
||||
<span class="sr-only"> (</span
|
||||
>{{
|
||||
(account.value.authenticationStatus === authStatus.Unlocked ? "unlocked" : "locked")
|
||||
| i18n
|
||||
|
||||
@@ -17,6 +17,8 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
||||
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
||||
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
|
||||
|
||||
type ActiveAccount = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -90,6 +92,7 @@ export class AccountSwitcherComponent implements OnInit {
|
||||
private environmentService: EnvironmentService,
|
||||
private loginEmailService: LoginEmailServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
private biometricsService: DesktopBiometricsService,
|
||||
) {
|
||||
this.activeAccount$ = this.accountService.activeAccount$.pipe(
|
||||
switchMap(async (active) => {
|
||||
@@ -107,7 +110,9 @@ export class AccountSwitcherComponent implements OnInit {
|
||||
name: active.name,
|
||||
email: active.email,
|
||||
avatarColor: await firstValueFrom(this.avatarService.avatarColor$),
|
||||
server: (await this.environmentService.getEnvironment())?.getHostname(),
|
||||
server: (
|
||||
await firstValueFrom(this.environmentService.getEnvironment$(active.id))
|
||||
)?.getHostname(),
|
||||
};
|
||||
}),
|
||||
);
|
||||
@@ -181,6 +186,7 @@ export class AccountSwitcherComponent implements OnInit {
|
||||
|
||||
async switch(userId: string) {
|
||||
this.close();
|
||||
await this.biometricsService.setShouldAutopromptNow(true);
|
||||
|
||||
this.disabled = true;
|
||||
const accountSwitchFinishedPromise = firstValueFrom(
|
||||
@@ -217,7 +223,9 @@ export class AccountSwitcherComponent implements OnInit {
|
||||
email: baseAccounts[userId].email,
|
||||
authenticationStatus: await this.authService.getAuthStatus(userId),
|
||||
avatarColor: await firstValueFrom(this.avatarService.getUserAvatarColor$(userId as UserId)),
|
||||
server: (await this.environmentService.getEnvironment(userId))?.getHostname(),
|
||||
server: (
|
||||
await firstValueFrom(this.environmentService.getEnvironment$(userId as UserId))
|
||||
)?.getHostname(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import "core-js/proposals/explicit-resource-management";
|
||||
|
||||
import { enableProdMode } from "@angular/core";
|
||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("../scss/styles.scss");
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
require("../scss/tailwind.css");
|
||||
|
||||
import { AppModule } from "./app.module";
|
||||
@@ -10,9 +14,7 @@ if (!ipc.platform.isDev) {
|
||||
enableProdMode();
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
platformBrowserDynamic().bootstrapModule(AppModule, { preserveWhitespaces: true });
|
||||
void platformBrowserDynamic().bootstrapModule(AppModule);
|
||||
|
||||
// Disable drag and drop to prevent malicious links from executing in the context of the app
|
||||
document.addEventListener("dragover", (event) => event.preventDefault());
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||
import { GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||
import {
|
||||
THEME_SELECTION,
|
||||
ThemeStateService,
|
||||
} from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
|
||||
export class DesktopThemeStateService implements ThemeStateService {
|
||||
private readonly selectedThemeState = this.globalStateProvider.get(THEME_SELECTION);
|
||||
|
||||
selectedTheme$ = this.selectedThemeState.state$.pipe(map((theme) => theme ?? this.defaultTheme));
|
||||
|
||||
constructor(
|
||||
private globalStateProvider: GlobalStateProvider,
|
||||
private defaultTheme: ThemeType = ThemeType.System,
|
||||
) {}
|
||||
|
||||
async setSelectedTheme(theme: ThemeType): Promise<void> {
|
||||
await this.selectedThemeState.update(() => theme, {
|
||||
shouldUpdate: (currentTheme) => currentTheme !== theme,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -5,24 +5,25 @@ import { firstValueFrom } from "rxjs";
|
||||
import { AbstractThemingService } from "@bitwarden/angular/platform/services/theming/theming.service.abstraction";
|
||||
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
|
||||
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||
import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { I18nService as I18nServiceAbstraction } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { NotificationsService } from "@bitwarden/common/platform/notifications";
|
||||
import { ContainerService } from "@bitwarden/common/platform/services/container.service";
|
||||
import { UserAutoUnlockKeyService } from "@bitwarden/common/platform/services/user-auto-unlock-key.service";
|
||||
import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/platform/sync";
|
||||
import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/services/vault-timeout/vault-timeout.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { KeyService as KeyServiceAbstraction } from "@bitwarden/key-management";
|
||||
|
||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
|
||||
import { SshAgentService } from "../../autofill/services/ssh-agent.service";
|
||||
import { I18nRendererService } from "../../platform/services/i18n.renderer.service";
|
||||
import { SshAgentService } from "../../platform/services/ssh-agent.service";
|
||||
import { VersionService } from "../../platform/services/version.service";
|
||||
import { NativeMessagingService } from "../../services/native-messaging.service";
|
||||
|
||||
@@ -31,11 +32,11 @@ export class InitService {
|
||||
constructor(
|
||||
@Inject(WINDOW) private win: Window,
|
||||
private syncService: SyncServiceAbstraction,
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
private vaultTimeoutService: DefaultVaultTimeoutService,
|
||||
private i18nService: I18nServiceAbstraction,
|
||||
private eventUploadService: EventUploadServiceAbstraction,
|
||||
private twoFactorService: TwoFactorServiceAbstraction,
|
||||
private notificationsService: NotificationsServiceAbstraction,
|
||||
private notificationsService: NotificationsService,
|
||||
private platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||
private stateService: StateServiceAbstraction,
|
||||
private keyService: KeyServiceAbstraction,
|
||||
@@ -47,11 +48,13 @@ export class InitService {
|
||||
private versionService: VersionService,
|
||||
private sshAgentService: SshAgentService,
|
||||
private autofillService: DesktopAutofillService,
|
||||
private sdkLoadService: SdkLoadService,
|
||||
@Inject(DOCUMENT) private document: Document,
|
||||
) {}
|
||||
|
||||
init() {
|
||||
return async () => {
|
||||
await this.sdkLoadService.loadAndInit();
|
||||
await this.sshAgentService.init();
|
||||
this.nativeMessagingService.init();
|
||||
await this.stateService.init({ runMigrations: false }); // Desktop will run them in main process
|
||||
@@ -75,7 +78,7 @@ export class InitService {
|
||||
await (this.i18nService as I18nRendererService).init();
|
||||
(this.eventUploadService as EventUploadService).init(true);
|
||||
this.twoFactorService.init();
|
||||
setTimeout(() => this.notificationsService.init(), 3000);
|
||||
this.notificationsService.startListening();
|
||||
const htmlEl = this.win.document.documentElement;
|
||||
htmlEl.classList.add("os_" + this.platformUtilsService.getDeviceString());
|
||||
this.themingService.applyThemeChangesTo(this.document);
|
||||
|
||||
@@ -25,18 +25,18 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.
|
||||
import {
|
||||
LoginComponentService,
|
||||
SetPasswordJitService,
|
||||
LockComponentService,
|
||||
SsoComponentService,
|
||||
DefaultSsoComponentService,
|
||||
TwoFactorAuthDuoComponentService,
|
||||
} from "@bitwarden/auth/angular";
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginApprovalComponentServiceAbstraction,
|
||||
LoginEmailService,
|
||||
PinServiceAbstraction,
|
||||
SsoUrlService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { PolicyService as PolicyServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import {
|
||||
@@ -52,10 +52,14 @@ import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/
|
||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { DefaultProcessReloadService } from "@bitwarden/common/key-management/services/default-process-reload.service";
|
||||
import {
|
||||
VaultTimeoutSettingsService,
|
||||
VaultTimeoutStringType,
|
||||
} from "@bitwarden/common/key-management/vault-timeout";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
|
||||
import { Fido2UserInterfaceService as Fido2UserInterfaceServiceAbstraction } from "@bitwarden/common/platform/abstractions/fido2/fido2-user-interface.service.abstraction";
|
||||
@@ -69,6 +73,7 @@ import {
|
||||
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
|
||||
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
|
||||
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { AbstractStorageService } from "@bitwarden/common/platform/abstractions/storage.service";
|
||||
import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/platform/abstractions/system.service";
|
||||
@@ -79,14 +84,14 @@ import { TaskSchedulerService } from "@bitwarden/common/platform/scheduling";
|
||||
import { Fido2AuthenticatorService } from "@bitwarden/common/platform/services/fido2/fido2-authenticator.service";
|
||||
import { MemoryStorageService } from "@bitwarden/common/platform/services/memory-storage.service";
|
||||
import { DefaultSdkClientFactory } from "@bitwarden/common/platform/services/sdk/default-sdk-client-factory";
|
||||
import { DefaultSdkLoadService } from "@bitwarden/common/platform/services/sdk/default-sdk-load.service";
|
||||
import { NoopSdkClientFactory } from "@bitwarden/common/platform/services/sdk/noop-sdk-client-factory";
|
||||
import { NoopSdkLoadService } from "@bitwarden/common/platform/services/sdk/noop-sdk-load.service";
|
||||
import { SystemService } from "@bitwarden/common/platform/services/system.service";
|
||||
import { GlobalStateProvider, StateProvider } from "@bitwarden/common/platform/state";
|
||||
// eslint-disable-next-line import/no-restricted-paths -- Implementation for memory storage
|
||||
import { MemoryStorageService as MemoryStorageServiceForStateProviders } from "@bitwarden/common/platform/state/storage/memory-storage.service";
|
||||
import { SyncService } from "@bitwarden/common/platform/sync";
|
||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
@@ -97,13 +102,18 @@ import {
|
||||
BiometricStateService,
|
||||
BiometricsService,
|
||||
} from "@bitwarden/key-management";
|
||||
import { LockComponentService } from "@bitwarden/key-management-ui";
|
||||
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
|
||||
|
||||
import { DesktopLoginApprovalComponentService } from "../../auth/login/desktop-login-approval-component.service";
|
||||
import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service";
|
||||
import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service";
|
||||
import { DesktopFido2UserInterfaceService } from "../../autofill/services/desktop-fido2-user-interface.service";
|
||||
import { ElectronBiometricsService } from "../../key-management/biometrics/electron-biometrics.service";
|
||||
import { DesktopBiometricsService } from "../../key-management/biometrics/desktop.biometrics.service";
|
||||
import { RendererBiometricsService } from "../../key-management/biometrics/renderer-biometrics.service";
|
||||
import { DesktopLockComponentService } from "../../key-management/lock/services/desktop-lock-component.service";
|
||||
import { flagEnabled } from "../../platform/flags";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
import { ElectronKeyService } from "../../platform/services/electron-key.service";
|
||||
@@ -119,7 +129,6 @@ import { I18nRendererService } from "../../platform/services/i18n.renderer.servi
|
||||
import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging";
|
||||
import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme";
|
||||
import { BiometricMessageHandlerService } from "../../services/biometric-message-handler.service";
|
||||
import { DesktopLockComponentService } from "../../services/desktop-lock-component.service";
|
||||
import { DuckDuckGoMessageHandlerService } from "../../services/duckduckgo-message-handler.service";
|
||||
import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service";
|
||||
import { NativeMessagingService } from "../../services/native-messaging.service";
|
||||
@@ -127,7 +136,6 @@ import { SearchBarService } from "../layout/search/search-bar.service";
|
||||
|
||||
import { DesktopFileDownloadService } from "./desktop-file-download.service";
|
||||
import { DesktopSetPasswordJitService } from "./desktop-set-password-jit.service";
|
||||
import { DesktopThemeStateService } from "./desktop-theme.service";
|
||||
import { InitService } from "./init.service";
|
||||
import { NativeMessagingManifestService } from "./native-messaging-manifest.service";
|
||||
import { RendererCryptoFunctionService } from "./renderer-crypto-function.service";
|
||||
@@ -143,7 +151,12 @@ const safeProviders: SafeProvider[] = [
|
||||
safeProvider(InitService),
|
||||
safeProvider({
|
||||
provide: BiometricsService,
|
||||
useClass: ElectronBiometricsService,
|
||||
useClass: RendererBiometricsService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: DesktopBiometricsService,
|
||||
useClass: RendererBiometricsService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider(NativeMessagingService),
|
||||
@@ -242,6 +255,7 @@ const safeProviders: SafeProvider[] = [
|
||||
VaultTimeoutSettingsService,
|
||||
BiometricStateService,
|
||||
AccountServiceAbstraction,
|
||||
LogService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -254,11 +268,6 @@ const safeProviders: SafeProvider[] = [
|
||||
useFactory: () => fromIpcSystemTheme(),
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ThemeStateService,
|
||||
useClass: DesktopThemeStateService,
|
||||
deps: [GlobalStateProvider],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: EncryptedMessageHandlerService,
|
||||
deps: [
|
||||
@@ -303,6 +312,7 @@ const safeProviders: SafeProvider[] = [
|
||||
StateProvider,
|
||||
BiometricStateService,
|
||||
KdfConfigService,
|
||||
DesktopBiometricsService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -380,6 +390,11 @@ const safeProviders: SafeProvider[] = [
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SsoUrlService,
|
||||
useClass: SsoUrlService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginComponentService,
|
||||
useClass: DesktopLoginComponentService,
|
||||
@@ -391,6 +406,17 @@ const safeProviders: SafeProvider[] = [
|
||||
SsoLoginServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
ToastService,
|
||||
SsoUrlService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: TwoFactorAuthDuoComponentService,
|
||||
useClass: DesktopTwoFactorAuthDuoComponentService,
|
||||
deps: [
|
||||
MessageListener,
|
||||
EnvironmentService,
|
||||
I18nServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
@@ -398,6 +424,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: flagEnabled("sdk") ? DefaultSdkClientFactory : NoopSdkClientFactory,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SdkLoadService,
|
||||
useClass: flagEnabled("sdk") ? DefaultSdkLoadService : NoopSdkLoadService,
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LoginEmailService,
|
||||
useClass: LoginEmailService,
|
||||
@@ -413,6 +444,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DesktopLoginApprovalComponentService,
|
||||
deps: [I18nServiceAbstraction],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SshImportPromptService,
|
||||
useClass: DefaultSshImportPromptService,
|
||||
deps: [DialogService, ToastService, PlatformUtilsServiceAbstraction, I18nServiceAbstraction],
|
||||
}),
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
||||
@@ -1,636 +0,0 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="generatorTitle">
|
||||
<div class="modal-dialog modal-md" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<h1 class="modal-title" id="generatorTitle">
|
||||
{{ "generator" | i18n }}
|
||||
</h1>
|
||||
<bit-callout
|
||||
type="info"
|
||||
*ngIf="enforcedPasswordPolicyOptions?.inEffect() && type === 'password'"
|
||||
>
|
||||
{{ "passwordGeneratorPolicyInEffect" | i18n }}
|
||||
</bit-callout>
|
||||
<div class="generated-block" *ngIf="type === 'password'">
|
||||
<div
|
||||
class="generated-wrapper"
|
||||
[innerHTML]="password | colorPassword"
|
||||
[appCopyText]="password"
|
||||
></div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn primary"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy()"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn primary"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'regeneratePassword' | i18n }}"
|
||||
(click)="regenerate()"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-generate" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="generated-block" *ngIf="type === 'username'">
|
||||
<div
|
||||
class="generated-wrapper"
|
||||
[innerHTML]="username | colorPassword"
|
||||
[appCopyText]="username"
|
||||
></div>
|
||||
<div class="action-buttons" #form [appApiAction]="usernameGeneratingPromise">
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn primary"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyUsername' | i18n }}"
|
||||
(click)="copy()"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="icon-btn primary"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'regenerateUsername' | i18n }}"
|
||||
(click)="$any(form).loading ? false : regenerate()"
|
||||
[attr.aria-disabled]="$any(form).loading ? 'true' : null"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg bwi-generate"
|
||||
[ngClass]="$any(form).loading ? 'bwi-spin' : ''"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="!comingFromAddEdit">
|
||||
<div class="box-content condensed">
|
||||
<div
|
||||
class="box-content-row box-content-row-radio"
|
||||
role="radiogroup"
|
||||
aria-labelledby="typeHeading"
|
||||
>
|
||||
<label id="typeHeading" class="radio-header">{{
|
||||
"whatWouldYouLikeToGenerate" | i18n
|
||||
}}</label>
|
||||
<div
|
||||
class="radio-group text-default"
|
||||
appBoxRow
|
||||
name="TypeOptions"
|
||||
*ngFor="let o of typeOptions"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
class="radio"
|
||||
[(ngModel)]="type"
|
||||
name="Type"
|
||||
id="type_{{ o.value }}"
|
||||
[value]="o.value"
|
||||
(change)="typeChanged()"
|
||||
[checked]="type === o.value"
|
||||
/>
|
||||
<label class="unstyled" for="type_{{ o.value }}">
|
||||
{{ o.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="type === 'password'">
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
<button type="button" (click)="toggleOptions()" [attr.aria-expanded]="showOptions">
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-angle-right': !showOptions, 'bwi-angle-down': showOptions }"
|
||||
></i>
|
||||
{{ "options" | i18n }}
|
||||
</button>
|
||||
</h2>
|
||||
<div class="box-content condensed" [hidden]="!showOptions">
|
||||
<div
|
||||
class="box-content-row box-content-row-radio"
|
||||
role="radiogroup"
|
||||
aria-labelledby="passwordTypeHeading"
|
||||
>
|
||||
<label id="passwordTypeHeading" class="radio-header">{{
|
||||
"passwordType" | i18n
|
||||
}}</label>
|
||||
<div
|
||||
class="radio-group text-default"
|
||||
appBoxRow
|
||||
name="PassTypeOptions"
|
||||
*ngFor="let o of passTypeOptions"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
class="radio"
|
||||
[(ngModel)]="passwordOptions.type"
|
||||
name="PasswordType"
|
||||
id="passwordType_{{ o.value }}"
|
||||
[value]="o.value"
|
||||
(change)="savePasswordOptions()"
|
||||
[checked]="passwordOptions.type === o.value"
|
||||
/>
|
||||
<label class="unstyled" for="passwordType_{{ o.value }}">
|
||||
{{ o.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" [hidden]="!showOptions" *ngIf="passwordOptions.type === 'passphrase'">
|
||||
<div class="box-content condensed">
|
||||
<div class="box-content-row box-content-row-input" appBoxRow>
|
||||
<label for="num-words">{{ "numWords" | i18n }}</label>
|
||||
<input
|
||||
id="num-words"
|
||||
type="number"
|
||||
min="3"
|
||||
max="20"
|
||||
(change)="savePasswordOptions()"
|
||||
[(ngModel)]="passwordOptions.numWords"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-input" appBoxRow>
|
||||
<label for="word-separator">{{ "wordSeparator" | i18n }}</label>
|
||||
<input
|
||||
id="word-separator"
|
||||
type="text"
|
||||
maxlength="1"
|
||||
(input)="savePasswordOptions()"
|
||||
[(ngModel)]="passwordOptions.wordSeparator"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="capitalize">{{ "capitalize" | i18n }}</label>
|
||||
<input
|
||||
id="capitalize"
|
||||
type="checkbox"
|
||||
(change)="savePasswordOptions()"
|
||||
[(ngModel)]="passwordOptions.capitalize"
|
||||
[disabled]="enforcedPasswordPolicyOptions?.capitalize"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="include-number">{{ "includeNumber" | i18n }}</label>
|
||||
<input
|
||||
id="include-number"
|
||||
type="checkbox"
|
||||
(change)="savePasswordOptions()"
|
||||
[(ngModel)]="passwordOptions.includeNumber"
|
||||
[disabled]="enforcedPasswordPolicyOptions?.includeNumber"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="passwordOptions.type === 'password'">
|
||||
<div class="box" [hidden]="!showOptions">
|
||||
<div class="box-content condensed">
|
||||
<div class="box-content-row box-content-row-slider" appBoxRow>
|
||||
<label for="length">{{ "length" | i18n }}</label>
|
||||
<input
|
||||
id="length"
|
||||
type="number"
|
||||
[min]="passwordOptions.minLength"
|
||||
max="128"
|
||||
[(ngModel)]="passwordOptions.length"
|
||||
(blur)="savePasswordOptions()"
|
||||
/>
|
||||
<input
|
||||
id="lengthRange"
|
||||
type="range"
|
||||
[min]="passwordOptions.minLength"
|
||||
max="128"
|
||||
step="1"
|
||||
[(ngModel)]="passwordOptions.length"
|
||||
(change)="sliderChanged()"
|
||||
(input)="sliderInput()"
|
||||
attr.aria-label="{{ 'length' | i18n }}"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<span>{{ "passwordMinLength" | i18n }}</span>
|
||||
<span class="txt-right">{{ passwordOptions.minLength }}</span>
|
||||
<span
|
||||
class="sr-only"
|
||||
attr.aria-label="{{ 'passwordMinLength' | i18n }}"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{{ passwordOptionsMinLengthForReader$ | async }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="uppercase">A-Z</label>
|
||||
<input
|
||||
id="uppercase"
|
||||
type="checkbox"
|
||||
(change)="savePasswordOptions()"
|
||||
[disabled]="enforcedPasswordPolicyOptions?.useUppercase"
|
||||
[(ngModel)]="passwordOptions.uppercase"
|
||||
attr.aria-label="{{ 'uppercase' | i18n }}"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="lowercase">a-z</label>
|
||||
<input
|
||||
id="lowercase"
|
||||
type="checkbox"
|
||||
(change)="savePasswordOptions()"
|
||||
[disabled]="enforcedPasswordPolicyOptions?.useLowercase"
|
||||
[(ngModel)]="passwordOptions.lowercase"
|
||||
attr.aria-label="{{ 'lowercase' | i18n }}"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="numbers">0-9</label>
|
||||
<input
|
||||
id="numbers"
|
||||
type="checkbox"
|
||||
(change)="savePasswordOptions()"
|
||||
[disabled]="enforcedPasswordPolicyOptions?.useNumbers"
|
||||
[ngModel]="passwordOptions.number"
|
||||
(ngModelChange)="setPasswordOptionsNumber($event)"
|
||||
attr.aria-label="{{ 'numbers' | i18n }}"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="special">!@#$%^&*</label>
|
||||
<input
|
||||
id="special"
|
||||
type="checkbox"
|
||||
(change)="savePasswordOptions()"
|
||||
[disabled]="enforcedPasswordPolicyOptions?.useSpecial"
|
||||
[ngModel]="passwordOptions.special"
|
||||
(ngModelChange)="setPasswordOptionsSpecial($event)"
|
||||
attr.aria-label="{{ 'specialCharacters' | i18n }}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" [hidden]="!showOptions">
|
||||
<div class="box-content condensed">
|
||||
<div class="box-content-row box-content-row-input" appBoxRow>
|
||||
<label for="min-number">{{ "minNumbers" | i18n }}</label>
|
||||
<input
|
||||
id="min-number"
|
||||
type="number"
|
||||
min="0"
|
||||
max="9"
|
||||
(change)="savePasswordOptions()"
|
||||
[(ngModel)]="passwordOptions.minNumber"
|
||||
(input)="onPasswordOptionsMinNumberInput($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-input" appBoxRow>
|
||||
<label for="min-special">{{ "minSpecial" | i18n }}</label>
|
||||
<input
|
||||
id="min-special"
|
||||
type="number"
|
||||
min="0"
|
||||
max="9"
|
||||
(change)="savePasswordOptions()"
|
||||
[(ngModel)]="passwordOptions.minSpecial"
|
||||
(input)="onPasswordOptionsMinSpecialInput($event)"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="ambiguous">{{ "ambiguous" | i18n }}</label>
|
||||
<input
|
||||
id="ambiguous"
|
||||
type="checkbox"
|
||||
(change)="savePasswordOptions()"
|
||||
[(ngModel)]="avoidAmbiguous"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="type === 'username'">
|
||||
<div class="box">
|
||||
<h2 class="box-header">
|
||||
<button type="button" (click)="toggleOptions()" [attr.aria-expanded]="showOptions">
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-angle-right': !showOptions, 'bwi-angle-down': showOptions }"
|
||||
></i>
|
||||
{{ "options" | i18n }}
|
||||
</button>
|
||||
</h2>
|
||||
<div class="box-content condensed" [hidden]="!showOptions">
|
||||
<div
|
||||
class="box-content-row box-content-row-radio"
|
||||
role="radiogroup"
|
||||
aria-labelledby="usernameTypeHeading"
|
||||
>
|
||||
<label id="usernameTypeHeading" class="radio-header">
|
||||
{{ "usernameType" | i18n }}
|
||||
<a
|
||||
href="#"
|
||||
appStopClick
|
||||
(click)="usernameTypesLearnMore()"
|
||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||
</a>
|
||||
</label>
|
||||
<div
|
||||
class="radio-group align-start text-default"
|
||||
appBoxRow
|
||||
name="UsernameTypeOptions"
|
||||
*ngFor="let o of usernameTypeOptions"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
class="radio"
|
||||
[(ngModel)]="usernameOptions.type"
|
||||
name="UsernameType"
|
||||
id="usernameType_{{ o.value }}"
|
||||
[value]="o.value"
|
||||
(change)="saveUsernameOptions()"
|
||||
[checked]="usernameOptions.type === o.value"
|
||||
/>
|
||||
<label class="unstyled" for="usernameType_{{ o.value }}">
|
||||
{{ o.name }}
|
||||
<small class="help-block" *ngIf="o.desc">{{ o.desc }}</small>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="usernameOptions.type === 'forwarded'" [hidden]="!showOptions">
|
||||
<div class="box-content condensed">
|
||||
<div class="box-content-row" role="listbox" aria-labelledby="forwardTypeHeading">
|
||||
<label id="forwardTypeHeading">{{ "service" | i18n }}</label>
|
||||
<select
|
||||
id="ForwardTypeDropdown"
|
||||
name="ForwardType"
|
||||
[(ngModel)]="usernameOptions.forwardedService"
|
||||
(change)="saveUsernameOptions()"
|
||||
>
|
||||
<option *ngFor="let o of forwardOptions" [ngValue]="o.value" role="option">
|
||||
{{ o.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<ng-container *ngIf="usernameOptions.forwardedService === 'simplelogin'">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="simplelogin-apikey">{{ "apiKey" | i18n }}</label>
|
||||
<input
|
||||
id="simplelogin-apikey"
|
||||
type="password"
|
||||
name="SimpleLoginApiKey"
|
||||
[(ngModel)]="usernameOptions.forwardedSimpleLoginApiKey"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="simplelogin-baseUrl">{{ "baseUrl" | i18n }}</label>
|
||||
<input
|
||||
id="simplelogin-baseUrl"
|
||||
type="text"
|
||||
name="SimpleLoginDomain"
|
||||
[(ngModel)]="usernameOptions.forwardedSimpleLoginBaseUrl"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="usernameOptions.forwardedService === 'duckduckgo'">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="duckduckgo-apikey">{{ "apiKey" | i18n }}</label>
|
||||
<input
|
||||
id="duckduckgo-apikey"
|
||||
type="password"
|
||||
name="DuckDuckGoApiKey"
|
||||
[(ngModel)]="usernameOptions.forwardedDuckDuckGoToken"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="usernameOptions.forwardedService === 'anonaddy'">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="anonaddy-accessToken">{{ "apiAccessToken" | i18n }}</label>
|
||||
<input
|
||||
id="anonaddy-accessToken"
|
||||
type="password"
|
||||
name="AnonAddyAccessToken"
|
||||
[(ngModel)]="usernameOptions.forwardedAnonAddyApiToken"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="anonaddy-domain">{{ "aliasDomain" | i18n }}</label>
|
||||
<input
|
||||
id="anonaddy-domain"
|
||||
type="text"
|
||||
name="AnonAddyDomain"
|
||||
[(ngModel)]="usernameOptions.forwardedAnonAddyDomain"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="anonaddy-baseUrl">{{ "baseUrl" | i18n }}</label>
|
||||
<input
|
||||
id="anonaddy-baseUrl"
|
||||
type="text"
|
||||
name="AnonAddyDomain"
|
||||
[(ngModel)]="usernameOptions.forwardedAnonAddyBaseUrl"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="usernameOptions.forwardedService === 'firefoxrelay'">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="firefox-apikey">{{ "apiAccessToken" | i18n }}</label>
|
||||
<input
|
||||
id="firefox-apikey"
|
||||
type="password"
|
||||
name="FirefoxApiKey"
|
||||
[(ngModel)]="usernameOptions.forwardedFirefoxApiToken"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="usernameOptions.forwardedService === 'fastmail'">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="fastmail-apiToken">{{ "apiAccessToken" | i18n }}</label>
|
||||
<input
|
||||
id="fastmail-apiToken"
|
||||
type="password"
|
||||
name="FastmailApiToken"
|
||||
[(ngModel)]="usernameOptions.forwardedFastmailApiToken"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="usernameOptions.forwardedService === 'forwardemail'">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="forwardemail-accessToken">{{ "apiAccessToken" | i18n }}</label>
|
||||
<input
|
||||
id="forwardemail-accessToken"
|
||||
type="password"
|
||||
name="ForwardEmailAccessToken"
|
||||
[(ngModel)]="usernameOptions.forwardedForwardEmailApiToken"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="forwardemail-domain">{{ "aliasDomain" | i18n }}</label>
|
||||
<input
|
||||
id="forwardemail-domain"
|
||||
type="text"
|
||||
name="ForwardEmailDomain"
|
||||
[(ngModel)]="usernameOptions.forwardedForwardEmailDomain"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="usernameOptions.type === 'subaddress'" [hidden]="!showOptions">
|
||||
<div class="box-content condensed">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="subaddress-email">{{ "emailAddress" | i18n }}</label>
|
||||
<input
|
||||
id="subaddress-email"
|
||||
type="text"
|
||||
name="SubaddressEmail"
|
||||
[(ngModel)]="usernameOptions.subaddressEmail"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="box-content-row"
|
||||
role="radiogroup"
|
||||
aria-labelledby="subaddressTypeHeading"
|
||||
*ngIf="subaddressOptions.length > 1"
|
||||
>
|
||||
<label id="subaddressTypeHeading" class="radio-header">{{ "type" | i18n }}</label>
|
||||
<div class="radio-group text-default" appBoxRow *ngFor="let o of subaddressOptions">
|
||||
<input
|
||||
type="radio"
|
||||
[(ngModel)]="usernameOptions.subaddressType"
|
||||
name="SubaddressType"
|
||||
id="subaddresstype_{{ o.value }}"
|
||||
[value]="o.value"
|
||||
(change)="saveUsernameOptions()"
|
||||
[checked]="usernameOptions.subaddressType === o.value"
|
||||
/>
|
||||
<label for="subaddresstype_{{ o.value }}">
|
||||
{{ o.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow *ngIf="usernameWebsite">
|
||||
<label for="subaddress-website">{{ "website" | i18n }}</label>
|
||||
<input
|
||||
id="subaddress-website"
|
||||
type="text"
|
||||
name="SubaddressWebsite"
|
||||
[value]="usernameOptions.website"
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="usernameOptions.type === 'catchall'" [hidden]="!showOptions">
|
||||
<div class="box-content condensed">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="catchall-domain">{{ "domainName" | i18n }}</label>
|
||||
<input
|
||||
id="catchall-domain"
|
||||
type="text"
|
||||
name="CatchallDomain"
|
||||
[(ngModel)]="usernameOptions.catchallDomain"
|
||||
(blur)="saveUsernameOptions()"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="box-content-row"
|
||||
role="radiogroup"
|
||||
aria-labelledby="catchallTypeHeading"
|
||||
*ngIf="catchallOptions.length > 1"
|
||||
>
|
||||
<label id="catchallTypeHeading" class="radio-header">{{ "type" | i18n }}</label>
|
||||
<div class="radio-group text-default" appBoxRow *ngFor="let o of catchallOptions">
|
||||
<input
|
||||
type="radio"
|
||||
[(ngModel)]="usernameOptions.catchallType"
|
||||
name="CatchallType"
|
||||
id="catchalltype_{{ o.value }}"
|
||||
[value]="o.value"
|
||||
(change)="saveUsernameOptions()"
|
||||
[checked]="usernameOptions.catchallType === o.value"
|
||||
/>
|
||||
<label for="catchalltype_{{ o.value }}">
|
||||
{{ o.name }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" appBoxRow *ngIf="usernameWebsite">
|
||||
<label for="catchall-website">{{ "website" | i18n }}</label>
|
||||
<input
|
||||
id="catchall-website"
|
||||
type="text"
|
||||
name="CatchallWebsite"
|
||||
[value]="usernameOptions.website"
|
||||
disabled
|
||||
readonly
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box" *ngIf="usernameOptions.type === 'word'" [hidden]="!showOptions">
|
||||
<div class="box-content condensed">
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="capitalize">{{ "capitalize" | i18n }}</label>
|
||||
<input
|
||||
id="capitalize"
|
||||
type="checkbox"
|
||||
(change)="saveUsernameOptions()"
|
||||
[(ngModel)]="usernameOptions.wordCapitalize"
|
||||
/>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="include-number">{{ "includeNumber" | i18n }}</label>
|
||||
<input
|
||||
id="include-number"
|
||||
type="checkbox"
|
||||
(change)="saveUsernameOptions()"
|
||||
[(ngModel)]="usernameOptions.wordIncludeNumber"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
*ngIf="comingFromAddEdit"
|
||||
(click)="select()"
|
||||
appA11yTitle="{{ 'select' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-fw bwi-check" aria-hidden="true"></i>
|
||||
</button>
|
||||
<button type="button" data-dismiss="modal">
|
||||
{{ (comingFromAddEdit ? "cancel" : "close") | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,90 +0,0 @@
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
|
||||
import { GeneratorComponent } from "./generator.component";
|
||||
|
||||
describe("GeneratorComponent", () => {
|
||||
let component: GeneratorComponent;
|
||||
let fixture: ComponentFixture<GeneratorComponent>;
|
||||
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
|
||||
|
||||
beforeEach(() => {
|
||||
platformUtilsServiceMock = mock<PlatformUtilsService>();
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [GeneratorComponent, I18nPipe],
|
||||
providers: [
|
||||
{
|
||||
provide: PasswordGenerationServiceAbstraction,
|
||||
useValue: mock<PasswordGenerationServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: UsernameGenerationServiceAbstraction,
|
||||
useValue: mock<UsernameGenerationServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: platformUtilsServiceMock,
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: mock<ActivatedRoute>(),
|
||||
},
|
||||
{
|
||||
provide: LogService,
|
||||
useValue: mock<LogService>(),
|
||||
},
|
||||
{
|
||||
provide: CipherService,
|
||||
useValue: mock<CipherService>(),
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: mock<AccountService>(),
|
||||
},
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: mock<ToastService>(),
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GeneratorComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("usernameTypesLearnMore()", () => {
|
||||
it("should call platformUtilsService.launchUri() once", () => {
|
||||
component.usernameTypesLearnMore();
|
||||
expect(platformUtilsServiceMock.launchUri).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import { Component, NgZone } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
|
||||
import { GeneratorComponent as BaseGeneratorComponent } from "@bitwarden/angular/tools/generator/components/generator.component";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
|
||||
@Component({
|
||||
selector: "app-generator",
|
||||
templateUrl: "generator.component.html",
|
||||
})
|
||||
export class GeneratorComponent extends BaseGeneratorComponent {
|
||||
constructor(
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
usernameGenerationService: UsernameGenerationServiceAbstraction,
|
||||
accountService: AccountService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
route: ActivatedRoute,
|
||||
ngZone: NgZone,
|
||||
logService: LogService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
passwordGenerationService,
|
||||
usernameGenerationService,
|
||||
platformUtilsService,
|
||||
accountService,
|
||||
i18nService,
|
||||
logService,
|
||||
route,
|
||||
ngZone,
|
||||
window,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
usernameTypesLearnMore() {
|
||||
this.platformUtilsService.launchUri("https://bitwarden.com/help/generator/#username-types");
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
|
||||
import { ImportComponent } from "@bitwarden/importer/ui";
|
||||
import { ImportComponent } from "@bitwarden/importer-ui";
|
||||
|
||||
@Component({
|
||||
templateUrl: "import-desktop.component.html",
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="passwordGenHistoryTitle">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<div class="box">
|
||||
<h1 class="box-header" id="passwordGenHistoryTitle">
|
||||
{{ "passwordHistory" | i18n }}
|
||||
</h1>
|
||||
<div class="box-content condensed">
|
||||
<div class="box-content-row box-content-row-flex" *ngFor="let h of history">
|
||||
<div class="row-main">
|
||||
<div
|
||||
class="password-wrapper monospaced"
|
||||
[appCopyText]="h.password"
|
||||
[innerHTML]="h.password | colorPassword"
|
||||
></div>
|
||||
<span class="detail">{{ h.date | date: "medium" }}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'copyPassword' | i18n }}"
|
||||
(click)="copy(h.password)"
|
||||
>
|
||||
<i class="bwi bwi-lg bwi-clone" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-content-row" *ngIf="!history.length">
|
||||
{{ "noPasswordsInList" | i18n }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
|
||||
<div class="right">
|
||||
<button
|
||||
type="button"
|
||||
(click)="clear()"
|
||||
class="danger"
|
||||
appA11yTitle="{{ 'clear' | i18n }}"
|
||||
>
|
||||
<i class="bwi bwi-trash bwi-lg bwi-fw" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Component } from "@angular/core";
|
||||
|
||||
import { PasswordGeneratorHistoryComponent as BasePasswordGeneratorHistoryComponent } from "@bitwarden/angular/tools/generator/components/password-generator-history.component";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@Component({
|
||||
selector: "app-password-generator-history",
|
||||
templateUrl: "password-generator-history.component.html",
|
||||
})
|
||||
export class PasswordGeneratorHistoryComponent extends BasePasswordGeneratorHistoryComponent {
|
||||
constructor(
|
||||
passwordGenerationService: PasswordGenerationServiceAbstraction,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
i18nService: I18nService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(passwordGenerationService, platformUtilsService, i18nService, window, toastService);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { Component, NgZone, OnDestroy, OnInit, ViewChild } from "@angular/core";
|
||||
import { SendComponent as BaseSendComponent } from "@bitwarden/angular/tools/send/send.component";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -52,6 +53,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
|
||||
sendApiService: SendApiService,
|
||||
dialogService: DialogService,
|
||||
toastService: ToastService,
|
||||
accountService: AccountService,
|
||||
) {
|
||||
super(
|
||||
sendService,
|
||||
@@ -65,6 +67,7 @@ export class SendComponent extends BaseSendComponent implements OnInit, OnDestro
|
||||
sendApiService,
|
||||
dialogService,
|
||||
toastService,
|
||||
accountService,
|
||||
);
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
this.searchBarService.searchText$.subscribe((searchText) => {
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
<form id="lock-page" (ngSubmit)="submit()">
|
||||
<div class="content">
|
||||
<p aria-hidden="true"><i class="bwi bwi-lock bwi-4x text-muted"></i></p>
|
||||
<p>{{ "yourVaultIsLocked" | i18n }}</p>
|
||||
<div class="box last">
|
||||
<div class="box-content">
|
||||
<div
|
||||
class="box-content-row box-content-row-flex"
|
||||
appBoxRow
|
||||
*ngIf="pinEnabled || masterPasswordEnabled"
|
||||
>
|
||||
<div class="row-main" *ngIf="pinEnabled">
|
||||
<label for="pin">{{ "pin" | i18n }}</label>
|
||||
<input
|
||||
id="pin"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
name="PIN"
|
||||
class="monospaced"
|
||||
[(ngModel)]="pin"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<div class="row-main" *ngIf="masterPasswordEnabled && !pinEnabled">
|
||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||
<input
|
||||
id="masterPassword"
|
||||
type="{{ showPassword ? 'text' : 'password' }}"
|
||||
name="MasterPassword"
|
||||
aria-describedby="masterPasswordHelp"
|
||||
class="monospaced"
|
||||
[(ngModel)]="masterPassword"
|
||||
required
|
||||
appInputVerbatim
|
||||
/>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button
|
||||
type="button"
|
||||
class="row-btn"
|
||||
appStopClick
|
||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||
[attr.aria-pressed]="showPassword"
|
||||
(click)="togglePassword()"
|
||||
>
|
||||
<i
|
||||
class="bwi bwi-lg"
|
||||
aria-hidden="true"
|
||||
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
|
||||
></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="masterPasswordHelp" class="box-footer">
|
||||
{{ "loggedInAsOn" | i18n: email : webVaultHostname }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttons with-rows">
|
||||
<div class="buttons-row" *ngIf="supportsBiometric && biometricLock && biometricReady">
|
||||
<button
|
||||
type="button"
|
||||
class="btn block"
|
||||
[ngClass]="{ 'primary font-weight-bold': !pinEnabled && !masterPasswordEnabled }"
|
||||
(click)="unlockBiometric()"
|
||||
>
|
||||
{{ biometricText | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons-row">
|
||||
<button type="submit" class="btn primary block" *ngIf="pinEnabled || masterPasswordEnabled">
|
||||
<i class="bwi bwi-unlock" aria-hidden="true"></i> <b>{{ "unlock" | i18n }}</b>
|
||||
</button>
|
||||
<button type="button" class="btn block" (click)="logOut()">
|
||||
{{ "logOut" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,478 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { NO_ERRORS_SCHEMA } from "@angular/core";
|
||||
import { ComponentFixture, TestBed, fakeAsync, tick } from "@angular/core/testing";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
BiometricsService as AbstractBiometricService,
|
||||
BiometricStateService,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { BiometricsService } from "../key-management/biometrics/biometrics.service";
|
||||
|
||||
import { LockComponent } from "./lock.component";
|
||||
|
||||
// ipc mock global
|
||||
const isWindowVisibleMock = jest.fn();
|
||||
(global as any).ipc = {
|
||||
platform: {
|
||||
isWindowVisible: isWindowVisibleMock,
|
||||
},
|
||||
keyManagement: {
|
||||
biometric: {
|
||||
enabled: jest.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("LockComponent", () => {
|
||||
let component: LockComponent;
|
||||
let fixture: ComponentFixture<LockComponent>;
|
||||
let stateServiceMock: MockProxy<StateService>;
|
||||
let biometricStateService: MockProxy<BiometricStateService>;
|
||||
let biometricsService: MockProxy<BiometricsService>;
|
||||
let messagingServiceMock: MockProxy<MessagingService>;
|
||||
let broadcasterServiceMock: MockProxy<BroadcasterService>;
|
||||
let platformUtilsServiceMock: MockProxy<PlatformUtilsService>;
|
||||
let activatedRouteMock: MockProxy<ActivatedRoute>;
|
||||
let mockMasterPasswordService: FakeMasterPasswordService;
|
||||
let mockToastService: MockProxy<ToastService>;
|
||||
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
beforeEach(async () => {
|
||||
stateServiceMock = mock<StateService>();
|
||||
|
||||
messagingServiceMock = mock<MessagingService>();
|
||||
broadcasterServiceMock = mock<BroadcasterService>();
|
||||
platformUtilsServiceMock = mock<PlatformUtilsService>();
|
||||
mockToastService = mock<ToastService>();
|
||||
|
||||
activatedRouteMock = mock<ActivatedRoute>();
|
||||
activatedRouteMock.queryParams = mock<ActivatedRoute["queryParams"]>();
|
||||
|
||||
mockMasterPasswordService = new FakeMasterPasswordService();
|
||||
|
||||
biometricStateService = mock();
|
||||
biometricStateService.dismissedRequirePasswordOnStartCallout$ = of(false);
|
||||
biometricStateService.promptAutomatically$ = of(false);
|
||||
biometricStateService.promptCancelled$ = of(false);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [LockComponent, I18nPipe],
|
||||
providers: [
|
||||
{ provide: InternalMasterPasswordServiceAbstraction, useValue: mockMasterPasswordService },
|
||||
{
|
||||
provide: I18nService,
|
||||
useValue: mock<I18nService>(),
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: platformUtilsServiceMock,
|
||||
},
|
||||
{
|
||||
provide: MessagingService,
|
||||
useValue: messagingServiceMock,
|
||||
},
|
||||
{
|
||||
provide: KeyService,
|
||||
useValue: mock<KeyService>(),
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutService,
|
||||
useValue: mock<VaultTimeoutService>(),
|
||||
},
|
||||
{
|
||||
provide: VaultTimeoutSettingsService,
|
||||
useValue: mock<VaultTimeoutSettingsService>(),
|
||||
},
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: mock<EnvironmentService>(),
|
||||
},
|
||||
{
|
||||
provide: StateService,
|
||||
useValue: stateServiceMock,
|
||||
},
|
||||
{
|
||||
provide: ApiService,
|
||||
useValue: mock<ApiService>(),
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: activatedRouteMock,
|
||||
},
|
||||
{
|
||||
provide: BroadcasterService,
|
||||
useValue: broadcasterServiceMock,
|
||||
},
|
||||
{
|
||||
provide: PolicyApiServiceAbstraction,
|
||||
useValue: mock<PolicyApiServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: InternalPolicyService,
|
||||
useValue: mock<InternalPolicyService>(),
|
||||
},
|
||||
{
|
||||
provide: PasswordStrengthServiceAbstraction,
|
||||
useValue: mock<PasswordStrengthServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: LogService,
|
||||
useValue: mock<LogService>(),
|
||||
},
|
||||
{
|
||||
provide: DialogService,
|
||||
useValue: mock<DialogService>(),
|
||||
},
|
||||
{
|
||||
provide: DeviceTrustServiceAbstraction,
|
||||
useValue: mock<DeviceTrustServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: UserVerificationService,
|
||||
useValue: mock<UserVerificationService>(),
|
||||
},
|
||||
{
|
||||
provide: PinServiceAbstraction,
|
||||
useValue: mock<PinServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: BiometricStateService,
|
||||
useValue: biometricStateService,
|
||||
},
|
||||
{
|
||||
provide: AbstractBiometricService,
|
||||
useValue: biometricsService,
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: accountService,
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: mock(),
|
||||
},
|
||||
{
|
||||
provide: KdfConfigService,
|
||||
useValue: mock<KdfConfigService>(),
|
||||
},
|
||||
{
|
||||
provide: SyncService,
|
||||
useValue: mock<SyncService>(),
|
||||
},
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: mockToastService,
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LockComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("ngOnInit", () => {
|
||||
it("should call super.ngOnInit() once", async () => {
|
||||
const superNgOnInitSpy = jest.spyOn(BaseLockComponent.prototype, "ngOnInit");
|
||||
await component.ngOnInit();
|
||||
expect(superNgOnInitSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should set "autoPromptBiometric" to true if "biometricState.promptAutomatically$" resolves to true', async () => {
|
||||
biometricStateService.promptAutomatically$ = of(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
expect(component["autoPromptBiometric"]).toBe(true);
|
||||
});
|
||||
|
||||
it('should set "autoPromptBiometric" to false if "biometricState.promptAutomatically$" resolves to false', async () => {
|
||||
biometricStateService.promptAutomatically$ = of(false);
|
||||
|
||||
await component.ngOnInit();
|
||||
expect(component["autoPromptBiometric"]).toBe(false);
|
||||
});
|
||||
|
||||
it('should set "biometricReady" to true if "stateService.getBiometricReady()" resolves to true', async () => {
|
||||
component["canUseBiometric"] = jest.fn().mockResolvedValue(true);
|
||||
|
||||
await component.ngOnInit();
|
||||
expect(component["biometricReady"]).toBe(true);
|
||||
});
|
||||
|
||||
it('should set "biometricReady" to false if "stateService.getBiometricReady()" resolves to false', async () => {
|
||||
component["canUseBiometric"] = jest.fn().mockResolvedValue(false);
|
||||
|
||||
await component.ngOnInit();
|
||||
expect(component["biometricReady"]).toBe(false);
|
||||
});
|
||||
|
||||
it("should call displayBiometricUpdateWarning", async () => {
|
||||
component["displayBiometricUpdateWarning"] = jest.fn();
|
||||
await component.ngOnInit();
|
||||
expect(component["displayBiometricUpdateWarning"]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call delayedAskForBiometric", async () => {
|
||||
component["delayedAskForBiometric"] = jest.fn();
|
||||
await component.ngOnInit();
|
||||
expect(component["delayedAskForBiometric"]).toHaveBeenCalledTimes(1);
|
||||
expect(component["delayedAskForBiometric"]).toHaveBeenCalledWith(500);
|
||||
});
|
||||
|
||||
it("should call delayedAskForBiometric when queryParams change", async () => {
|
||||
activatedRouteMock.queryParams = of({ promptBiometric: true });
|
||||
component["delayedAskForBiometric"] = jest.fn();
|
||||
await component.ngOnInit();
|
||||
|
||||
expect(component["delayedAskForBiometric"]).toHaveBeenCalledTimes(1);
|
||||
expect(component["delayedAskForBiometric"]).toHaveBeenCalledWith(500);
|
||||
});
|
||||
|
||||
it("should call messagingService.send", async () => {
|
||||
await component.ngOnInit();
|
||||
expect(messagingServiceMock.send).toHaveBeenCalledWith("getWindowIsFocused");
|
||||
});
|
||||
|
||||
describe("broadcasterService.subscribe", () => {
|
||||
it('should call onWindowHidden() when "broadcasterService.subscribe" is called with "windowHidden"', async () => {
|
||||
component["onWindowHidden"] = jest.fn();
|
||||
await component.ngOnInit();
|
||||
broadcasterServiceMock.subscribe.mock.calls[0][1]({ command: "windowHidden" });
|
||||
expect(component["onWindowHidden"]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is false', async () => {
|
||||
component["focusInput"] = jest.fn();
|
||||
component["deferFocus"] = null;
|
||||
await component.ngOnInit();
|
||||
broadcasterServiceMock.subscribe.mock.calls[0][1]({
|
||||
command: "windowIsFocused",
|
||||
windowIsFocused: true,
|
||||
} as any);
|
||||
expect(component["deferFocus"]).toBe(false);
|
||||
expect(component["focusInput"]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is true', async () => {
|
||||
component["focusInput"] = jest.fn();
|
||||
component["deferFocus"] = null;
|
||||
await component.ngOnInit();
|
||||
broadcasterServiceMock.subscribe.mock.calls[0][1]({
|
||||
command: "windowIsFocused",
|
||||
windowIsFocused: false,
|
||||
} as any);
|
||||
expect(component["deferFocus"]).toBe(true);
|
||||
expect(component["focusInput"]).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is true and deferFocus is true', async () => {
|
||||
component["focusInput"] = jest.fn();
|
||||
component["deferFocus"] = true;
|
||||
await component.ngOnInit();
|
||||
broadcasterServiceMock.subscribe.mock.calls[0][1]({
|
||||
command: "windowIsFocused",
|
||||
windowIsFocused: true,
|
||||
} as any);
|
||||
expect(component["deferFocus"]).toBe(false);
|
||||
expect(component["focusInput"]).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not call focusInput() when "broadcasterService.subscribe" is called with "windowIsFocused" is false and deferFocus is true', async () => {
|
||||
component["focusInput"] = jest.fn();
|
||||
component["deferFocus"] = true;
|
||||
await component.ngOnInit();
|
||||
broadcasterServiceMock.subscribe.mock.calls[0][1]({
|
||||
command: "windowIsFocused",
|
||||
windowIsFocused: false,
|
||||
} as any);
|
||||
expect(component["deferFocus"]).toBe(true);
|
||||
expect(component["focusInput"]).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("ngOnDestroy", () => {
|
||||
it("should call super.ngOnDestroy()", () => {
|
||||
const superNgOnDestroySpy = jest.spyOn(BaseLockComponent.prototype, "ngOnDestroy");
|
||||
component.ngOnDestroy();
|
||||
expect(superNgOnDestroySpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call broadcasterService.unsubscribe()", () => {
|
||||
component.ngOnDestroy();
|
||||
expect(broadcasterServiceMock.unsubscribe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("focusInput", () => {
|
||||
it('should call "focus" on #pin input if pinEnabled is true', () => {
|
||||
component["pinEnabled"] = true;
|
||||
global.document.getElementById = jest.fn().mockReturnValue({ focus: jest.fn() });
|
||||
component["focusInput"]();
|
||||
expect(global.document.getElementById).toHaveBeenCalledWith("pin");
|
||||
});
|
||||
|
||||
it('should call "focus" on #masterPassword input if pinEnabled is false', () => {
|
||||
component["pinEnabled"] = false;
|
||||
global.document.getElementById = jest.fn().mockReturnValue({ focus: jest.fn() });
|
||||
component["focusInput"]();
|
||||
expect(global.document.getElementById).toHaveBeenCalledWith("masterPassword");
|
||||
});
|
||||
});
|
||||
|
||||
describe("delayedAskForBiometric", () => {
|
||||
beforeEach(() => {
|
||||
component["supportsBiometric"] = true;
|
||||
component["autoPromptBiometric"] = true;
|
||||
});
|
||||
|
||||
it('should wait for "delay" milliseconds', fakeAsync(async () => {
|
||||
const delaySpy = jest.spyOn(global, "setTimeout");
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
component["delayedAskForBiometric"](5000);
|
||||
|
||||
tick(4000);
|
||||
component["biometricAsked"] = false;
|
||||
|
||||
tick(1000);
|
||||
component["biometricAsked"] = true;
|
||||
|
||||
expect(delaySpy).toHaveBeenCalledWith(expect.any(Function), 5000);
|
||||
}));
|
||||
|
||||
it('should return; if "params" is defined and "params.promptBiometric" is false', fakeAsync(async () => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
component["delayedAskForBiometric"](5000, { promptBiometric: false });
|
||||
tick(5000);
|
||||
expect(component["biometricAsked"]).toBe(false);
|
||||
}));
|
||||
|
||||
it('should not return; if "params" is defined and "params.promptBiometric" is true', fakeAsync(async () => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
component["delayedAskForBiometric"](5000, { promptBiometric: true });
|
||||
tick(5000);
|
||||
expect(component["biometricAsked"]).toBe(true);
|
||||
}));
|
||||
|
||||
it('should not return; if "params" is undefined', fakeAsync(async () => {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
component["delayedAskForBiometric"](5000);
|
||||
tick(5000);
|
||||
expect(component["biometricAsked"]).toBe(true);
|
||||
}));
|
||||
|
||||
it('should return; if "supportsBiometric" is false', fakeAsync(async () => {
|
||||
component["supportsBiometric"] = false;
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
component["delayedAskForBiometric"](5000);
|
||||
tick(5000);
|
||||
expect(component["biometricAsked"]).toBe(false);
|
||||
}));
|
||||
|
||||
it('should return; if "autoPromptBiometric" is false', fakeAsync(async () => {
|
||||
component["autoPromptBiometric"] = false;
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
component["delayedAskForBiometric"](5000);
|
||||
tick(5000);
|
||||
expect(component["biometricAsked"]).toBe(false);
|
||||
}));
|
||||
|
||||
it("should call unlockBiometric() if biometricAsked is false and window is visible", fakeAsync(async () => {
|
||||
isWindowVisibleMock.mockResolvedValue(true);
|
||||
component["unlockBiometric"] = jest.fn();
|
||||
component["biometricAsked"] = false;
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
component["delayedAskForBiometric"](5000);
|
||||
tick(5000);
|
||||
|
||||
expect(component["unlockBiometric"]).toHaveBeenCalledTimes(1);
|
||||
}));
|
||||
|
||||
it("should not call unlockBiometric() if biometricAsked is false and window is not visible", fakeAsync(async () => {
|
||||
isWindowVisibleMock.mockResolvedValue(false);
|
||||
component["unlockBiometric"] = jest.fn();
|
||||
component["biometricAsked"] = false;
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
component["delayedAskForBiometric"](5000);
|
||||
tick(5000);
|
||||
|
||||
expect(component["unlockBiometric"]).toHaveBeenCalledTimes(0);
|
||||
}));
|
||||
|
||||
it("should not call unlockBiometric() if biometricAsked is true", fakeAsync(async () => {
|
||||
isWindowVisibleMock.mockResolvedValue(true);
|
||||
component["unlockBiometric"] = jest.fn();
|
||||
component["biometricAsked"] = true;
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
component["delayedAskForBiometric"](5000);
|
||||
tick(5000);
|
||||
|
||||
expect(component["unlockBiometric"]).toHaveBeenCalledTimes(0);
|
||||
}));
|
||||
});
|
||||
|
||||
describe("canUseBiometric", () => {
|
||||
it("should call biometric.enabled with current active user", async () => {
|
||||
await component["canUseBiometric"]();
|
||||
|
||||
expect(ipc.keyManagement.biometric.enabled).toHaveBeenCalledWith(mockUserId);
|
||||
});
|
||||
});
|
||||
|
||||
it('onWindowHidden() should set "showPassword" to false', () => {
|
||||
component["showPassword"] = true;
|
||||
component["onWindowHidden"]();
|
||||
expect(component["showPassword"]).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,235 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { firstValueFrom, map, switchMap } from "rxjs";
|
||||
|
||||
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
|
||||
import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { DeviceType } from "@bitwarden/common/enums";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
import {
|
||||
KdfConfigService,
|
||||
KeyService,
|
||||
BiometricsService,
|
||||
BiometricStateService,
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
const BroadcasterSubscriptionId = "LockComponent";
|
||||
|
||||
@Component({
|
||||
selector: "app-lock",
|
||||
templateUrl: "lock.component.html",
|
||||
})
|
||||
export class LockComponent extends BaseLockComponent implements OnInit, OnDestroy {
|
||||
private deferFocus: boolean = null;
|
||||
protected biometricReady = false;
|
||||
private biometricAsked = false;
|
||||
private autoPromptBiometric = false;
|
||||
private timerId: any;
|
||||
|
||||
constructor(
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
router: Router,
|
||||
i18nService: I18nService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
keyService: KeyService,
|
||||
vaultTimeoutService: VaultTimeoutService,
|
||||
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
||||
environmentService: EnvironmentService,
|
||||
protected override stateService: StateService,
|
||||
apiService: ApiService,
|
||||
private route: ActivatedRoute,
|
||||
private broadcasterService: BroadcasterService,
|
||||
ngZone: NgZone,
|
||||
policyApiService: PolicyApiServiceAbstraction,
|
||||
policyService: InternalPolicyService,
|
||||
passwordStrengthService: PasswordStrengthServiceAbstraction,
|
||||
logService: LogService,
|
||||
dialogService: DialogService,
|
||||
deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
userVerificationService: UserVerificationService,
|
||||
pinService: PinServiceAbstraction,
|
||||
biometricStateService: BiometricStateService,
|
||||
biometricsService: BiometricsService,
|
||||
accountService: AccountService,
|
||||
authService: AuthService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
syncService: SyncService,
|
||||
toastService: ToastService,
|
||||
) {
|
||||
super(
|
||||
masterPasswordService,
|
||||
router,
|
||||
i18nService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
keyService,
|
||||
vaultTimeoutService,
|
||||
vaultTimeoutSettingsService,
|
||||
environmentService,
|
||||
stateService,
|
||||
apiService,
|
||||
logService,
|
||||
ngZone,
|
||||
policyApiService,
|
||||
policyService,
|
||||
passwordStrengthService,
|
||||
dialogService,
|
||||
deviceTrustService,
|
||||
userVerificationService,
|
||||
pinService,
|
||||
biometricStateService,
|
||||
biometricsService,
|
||||
accountService,
|
||||
authService,
|
||||
kdfConfigService,
|
||||
syncService,
|
||||
toastService,
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await super.ngOnInit();
|
||||
this.autoPromptBiometric = await firstValueFrom(
|
||||
this.biometricStateService.promptAutomatically$,
|
||||
);
|
||||
this.biometricReady = await this.canUseBiometric();
|
||||
|
||||
await this.displayBiometricUpdateWarning();
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.delayedAskForBiometric(500);
|
||||
this.route.queryParams.pipe(switchMap((params) => this.delayedAskForBiometric(500, params)));
|
||||
|
||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||
this.ngZone.run(() => {
|
||||
switch (message.command) {
|
||||
case "windowHidden":
|
||||
this.onWindowHidden();
|
||||
break;
|
||||
case "windowIsFocused":
|
||||
if (this.deferFocus === null) {
|
||||
this.deferFocus = !message.windowIsFocused;
|
||||
if (!this.deferFocus) {
|
||||
this.focusInput();
|
||||
}
|
||||
} else if (this.deferFocus && message.windowIsFocused) {
|
||||
this.focusInput();
|
||||
this.deferFocus = false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
});
|
||||
this.messagingService.send("getWindowIsFocused");
|
||||
|
||||
// start background listener until destroyed on interval
|
||||
this.timerId = setInterval(async () => {
|
||||
this.supportsBiometric = await this.biometricsService.supportsBiometric();
|
||||
this.biometricReady = await this.canUseBiometric();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
|
||||
clearInterval(this.timerId);
|
||||
}
|
||||
|
||||
onWindowHidden() {
|
||||
this.showPassword = false;
|
||||
}
|
||||
|
||||
private async delayedAskForBiometric(delay: number, params?: any) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
|
||||
if (params && !params.promptBiometric) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.supportsBiometric || !this.autoPromptBiometric || this.biometricAsked) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await firstValueFrom(this.biometricStateService.promptCancelled$)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.biometricAsked = true;
|
||||
if (await ipc.platform.isWindowVisible()) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.unlockBiometric();
|
||||
}
|
||||
}
|
||||
|
||||
private async canUseBiometric() {
|
||||
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id)));
|
||||
return await ipc.keyManagement.biometric.enabled(userId);
|
||||
}
|
||||
|
||||
private focusInput() {
|
||||
document.getElementById(this.pinEnabled ? "pin" : "masterPassword")?.focus();
|
||||
}
|
||||
|
||||
private async displayBiometricUpdateWarning(): Promise<void> {
|
||||
if (await firstValueFrom(this.biometricStateService.dismissedRequirePasswordOnStartCallout$)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.platformUtilsService.getDevice() !== DeviceType.WindowsDesktop) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (await firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)) {
|
||||
const response = await this.dialogService.openSimpleDialog({
|
||||
title: { key: "windowsBiometricUpdateWarningTitle" },
|
||||
content: { key: "windowsBiometricUpdateWarning" },
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
await this.biometricStateService.setRequirePasswordOnStart(response);
|
||||
if (response) {
|
||||
await this.biometricStateService.setPromptAutomatically(false);
|
||||
}
|
||||
this.supportsBiometric = await this.canUseBiometric();
|
||||
await this.biometricStateService.setDismissedRequirePasswordOnStartCallout();
|
||||
}
|
||||
}
|
||||
|
||||
get biometricText() {
|
||||
switch (this.platformUtilsService.getDevice()) {
|
||||
case DeviceType.MacOsDesktop:
|
||||
return "unlockWithTouchId";
|
||||
case DeviceType.WindowsDesktop:
|
||||
return "unlockWithWindowsHello";
|
||||
case DeviceType.LinuxDesktop:
|
||||
return "unlockWithPolkit";
|
||||
default:
|
||||
throw new Error("Unsupported platform");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,15 +51,15 @@ describe("DesktopLoginApprovalComponentService", () => {
|
||||
it("calls ipc.auth.loginRequest with correct parameters when window is not visible", async () => {
|
||||
const title = "Log in requested";
|
||||
const email = "test@bitwarden.com";
|
||||
const message = `Confirm login attempt for ${email}`;
|
||||
const message = `Confirm access attempt for ${email}`;
|
||||
const closeText = "Close";
|
||||
|
||||
const loginApprovalComponent = { email } as LoginApprovalComponent;
|
||||
i18nService.t.mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case "logInRequested":
|
||||
case "accountAccessRequested":
|
||||
return title;
|
||||
case "confirmLoginAtemptForMail":
|
||||
case "confirmAccessAttempt":
|
||||
return message;
|
||||
case "close":
|
||||
return closeText;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
|
||||
import { DefaultLoginApprovalComponentService } from "@bitwarden/auth/angular";
|
||||
@@ -15,12 +13,12 @@ export class DesktopLoginApprovalComponentService
|
||||
super();
|
||||
}
|
||||
|
||||
async showLoginRequestedAlertIfWindowNotVisible(email: string): Promise<void> {
|
||||
async showLoginRequestedAlertIfWindowNotVisible(email?: string): Promise<void> {
|
||||
const isVisible = await ipc.platform.isWindowVisible();
|
||||
if (!isVisible) {
|
||||
await ipc.auth.loginRequest(
|
||||
this.i18nService.t("logInRequested"),
|
||||
this.i18nService.t("confirmLoginAtemptForMail", email),
|
||||
this.i18nService.t("accountAccessRequested"),
|
||||
this.i18nService.t("confirmAccessAttempt", email),
|
||||
this.i18nService.t("close"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { DefaultLoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { SsoUrlService } from "@bitwarden/auth/common";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { ClientType } from "@bitwarden/common/enums";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import {
|
||||
Environment,
|
||||
@@ -41,8 +43,7 @@ describe("DesktopLoginComponentService", () => {
|
||||
let ssoLoginService: MockProxy<SsoLoginServiceAbstraction>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let toastService: MockProxy<ToastService>;
|
||||
|
||||
let superLaunchSsoBrowserWindowSpy: jest.SpyInstance;
|
||||
let ssoUrlService: MockProxy<SsoUrlService>;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
@@ -60,6 +61,8 @@ describe("DesktopLoginComponentService", () => {
|
||||
ssoLoginService = mock<SsoLoginServiceAbstraction>();
|
||||
i18nService = mock<I18nService>();
|
||||
toastService = mock<ToastService>();
|
||||
platformUtilsService.getClientType.mockReturnValue(ClientType.Desktop);
|
||||
ssoUrlService = mock<SsoUrlService>();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -74,6 +77,7 @@ describe("DesktopLoginComponentService", () => {
|
||||
ssoLoginService,
|
||||
i18nService,
|
||||
toastService,
|
||||
ssoUrlService,
|
||||
),
|
||||
},
|
||||
{ provide: DefaultLoginComponentService, useExisting: DesktopLoginComponentService },
|
||||
@@ -84,15 +88,11 @@ describe("DesktopLoginComponentService", () => {
|
||||
{ provide: SsoLoginServiceAbstraction, useValue: ssoLoginService },
|
||||
{ provide: I18nService, useValue: i18nService },
|
||||
{ provide: ToastService, useValue: toastService },
|
||||
{ provide: SsoUrlService, useValue: ssoUrlService },
|
||||
],
|
||||
});
|
||||
|
||||
service = TestBed.inject(DesktopLoginComponentService);
|
||||
|
||||
superLaunchSsoBrowserWindowSpy = jest.spyOn(
|
||||
DefaultLoginComponentService.prototype,
|
||||
"launchSsoBrowserWindow",
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -106,7 +106,7 @@ describe("DesktopLoginComponentService", () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe("launchSsoBrowserWindow", () => {
|
||||
describe("redirectToSso", () => {
|
||||
// Array of all permutations of isAppImage, isSnapStore, and isDev
|
||||
const permutations = [
|
||||
[true, false, false], // Case 1: isAppImage true
|
||||
@@ -125,36 +125,27 @@ describe("DesktopLoginComponentService", () => {
|
||||
(global as any).ipc.platform.isSnapStore = isSnapStore;
|
||||
(global as any).ipc.platform.isDev = isDev;
|
||||
|
||||
const email = "user@example.com";
|
||||
const clientId = "desktop";
|
||||
const codeChallenge = "testCodeChallenge";
|
||||
const codeVerifier = "testCodeVerifier";
|
||||
const email = "test@bitwarden.com";
|
||||
const state = "testState";
|
||||
const codeVerifierHash = new Uint8Array(64);
|
||||
const codeVerifier = "testCodeVerifier";
|
||||
const codeChallenge = "testCodeChallenge";
|
||||
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(state);
|
||||
passwordGenerationService.generatePassword.mockResolvedValueOnce(codeVerifier);
|
||||
cryptoFunctionService.hash.mockResolvedValueOnce(codeVerifierHash);
|
||||
jest.spyOn(Utils, "fromBufferToUrlB64").mockReturnValue(codeChallenge);
|
||||
|
||||
await service.launchSsoBrowserWindow(email, clientId);
|
||||
await service.redirectToSsoLogin(email);
|
||||
|
||||
if (isAppImage || isSnapStore || isDev) {
|
||||
expect(superLaunchSsoBrowserWindowSpy).not.toHaveBeenCalled();
|
||||
|
||||
// Assert that the standard logic is executed
|
||||
expect(ssoLoginService.setSsoEmail).toHaveBeenCalledWith(email);
|
||||
expect(passwordGenerationService.generatePassword).toHaveBeenCalledTimes(2);
|
||||
expect(cryptoFunctionService.hash).toHaveBeenCalledWith(codeVerifier, "sha256");
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
|
||||
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
|
||||
expect(ipc.platform.localhostCallbackService.openSsoPrompt).toHaveBeenCalledWith(
|
||||
codeChallenge,
|
||||
state,
|
||||
email,
|
||||
);
|
||||
} else {
|
||||
// If all values are false, expect the super method to be called
|
||||
expect(superLaunchSsoBrowserWindowSpy).toHaveBeenCalledWith(email, clientId);
|
||||
expect(ssoLoginService.setSsoState).toHaveBeenCalledWith(state);
|
||||
expect(ssoLoginService.setCodeVerifier).toHaveBeenCalledWith(codeVerifier);
|
||||
expect(platformUtilsService.launchUri).toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { DefaultLoginComponentService, LoginComponentService } from "@bitwarden/auth/angular";
|
||||
import { DESKTOP_SSO_CALLBACK, SsoUrlService } from "@bitwarden/auth/common";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
|
||||
|
||||
@@ -25,6 +26,7 @@ export class DesktopLoginComponentService
|
||||
protected ssoLoginService: SsoLoginServiceAbstraction,
|
||||
protected i18nService: I18nService,
|
||||
protected toastService: ToastService,
|
||||
protected ssoUrlService: SsoUrlService,
|
||||
) {
|
||||
super(
|
||||
cryptoFunctionService,
|
||||
@@ -33,38 +35,52 @@ export class DesktopLoginComponentService
|
||||
platformUtilsService,
|
||||
ssoLoginService,
|
||||
);
|
||||
this.clientType = this.platformUtilsService.getClientType();
|
||||
}
|
||||
|
||||
override async launchSsoBrowserWindow(email: string, clientId: "desktop"): Promise<void | null> {
|
||||
if (!ipc.platform.isAppImage && !ipc.platform.isSnapStore && !ipc.platform.isDev) {
|
||||
return super.launchSsoBrowserWindow(email, clientId);
|
||||
/**
|
||||
* On the desktop, redirecting to the SSO login page is done via a new browser window, opened
|
||||
* to the SSO component on the web client.
|
||||
* @param email the email of the user trying to log in, used to look up the org SSO identifier
|
||||
* @param state the state that will be used to verify the SSO login, which needs to be passed to the IdP
|
||||
* @param codeChallenge the challenge that will be verified after the code is returned from the IdP, which needs to be passed to the IdP
|
||||
*/
|
||||
protected override async redirectToSso(
|
||||
email: string,
|
||||
state: string,
|
||||
codeChallenge: string,
|
||||
): Promise<void> {
|
||||
// For platforms that cannot support a protocol-based (e.g. bitwarden://) callback, we use a localhost callback
|
||||
// Otherwise, we launch the SSO component in a browser window and wait for the callback
|
||||
if (ipc.platform.isAppImage || ipc.platform.isSnapStore || ipc.platform.isDev) {
|
||||
await this.initiateSsoThroughLocalhostCallback(email, state, codeChallenge);
|
||||
} else {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const webVaultUrl = env.getWebVaultUrl();
|
||||
|
||||
const redirectUri = DESKTOP_SSO_CALLBACK;
|
||||
|
||||
const ssoWebAppUrl = this.ssoUrlService.buildSsoUrl(
|
||||
webVaultUrl,
|
||||
this.clientType,
|
||||
redirectUri,
|
||||
state,
|
||||
codeChallenge,
|
||||
email,
|
||||
);
|
||||
|
||||
this.platformUtilsService.launchUri(ssoWebAppUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// Save email for SSO
|
||||
await this.ssoLoginService.setSsoEmail(email);
|
||||
|
||||
// Generate SSO params
|
||||
const passwordOptions: any = {
|
||||
type: "password",
|
||||
length: 64,
|
||||
uppercase: true,
|
||||
lowercase: true,
|
||||
numbers: true,
|
||||
special: false,
|
||||
};
|
||||
|
||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
||||
const codeVerifierHash = await this.cryptoFunctionService.hash(codeVerifier, "sha256");
|
||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
||||
|
||||
// Save SSO params
|
||||
await this.ssoLoginService.setSsoState(state);
|
||||
await this.ssoLoginService.setCodeVerifier(codeVerifier);
|
||||
|
||||
private async initiateSsoThroughLocalhostCallback(
|
||||
email: string,
|
||||
state: string,
|
||||
challenge: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(codeChallenge, state);
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(challenge, state, email);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (err) {
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</div>
|
||||
<div class="sub-options">
|
||||
<p class="no-margin">{{ "newAroundHere" | i18n }}</p>
|
||||
<button type="button" class="text text-primary" [routerLink]="registerRoute$ | async">
|
||||
<button type="button" class="text text-primary" routerLink="/signup">
|
||||
{{ "createAccount" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Component, NgZone, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { Subject, takeUntil, tap } from "rxjs";
|
||||
import { Subject, firstValueFrom, takeUntil, tap } from "rxjs";
|
||||
|
||||
import { LoginComponentV1 as BaseLoginComponent } from "@bitwarden/angular/auth/components/login-v1.component";
|
||||
import { FormValidationErrorsService } from "@bitwarden/angular/platform/abstractions/form-validation-errors.service";
|
||||
@@ -11,11 +11,9 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||
import {
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginEmailServiceAbstraction,
|
||||
RegisterRouteService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction";
|
||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||
import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||
@@ -77,8 +75,6 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
||||
route: ActivatedRoute,
|
||||
loginEmailService: LoginEmailServiceAbstraction,
|
||||
ssoLoginService: SsoLoginServiceAbstraction,
|
||||
webAuthnLoginService: WebAuthnLoginServiceAbstraction,
|
||||
registerRouteService: RegisterRouteService,
|
||||
toastService: ToastService,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
@@ -100,8 +96,6 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
||||
route,
|
||||
loginEmailService,
|
||||
ssoLoginService,
|
||||
webAuthnLoginService,
|
||||
registerRouteService,
|
||||
toastService,
|
||||
);
|
||||
this.onSuccessfulLogin = () => {
|
||||
@@ -149,10 +143,11 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
||||
.getFeatureFlag$(FeatureFlag.UnauthenticatedExtensionUIRefresh)
|
||||
.pipe(
|
||||
tap(async (flag) => {
|
||||
// If the flag is turned ON, we must force a reload to ensure the correct UI is shown
|
||||
if (flag) {
|
||||
const qParams = await firstValueFrom(this.route.queryParams);
|
||||
|
||||
const uniqueQueryParams = {
|
||||
...this.route.queryParams,
|
||||
...qParams,
|
||||
// adding a unique timestamp to the query params to force a reload
|
||||
t: new Date().getTime().toString(),
|
||||
};
|
||||
@@ -162,7 +157,7 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
||||
});
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
takeUntil(this.componentDestroyed$),
|
||||
)
|
||||
.subscribe();
|
||||
}
|
||||
@@ -226,9 +221,10 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
||||
if (!ipc.platform.isAppImage && !ipc.platform.isSnapStore && !ipc.platform.isDev) {
|
||||
return super.launchSsoBrowser(clientId, ssoRedirectUri);
|
||||
}
|
||||
const email = this.formGroup.controls.email.value;
|
||||
|
||||
// Save off email for SSO
|
||||
await this.ssoLoginService.setSsoEmail(this.formGroup.value.email);
|
||||
await this.ssoLoginService.setSsoEmail(email);
|
||||
|
||||
// Generate necessary sso params
|
||||
const passwordOptions: any = {
|
||||
@@ -247,8 +243,11 @@ export class LoginComponentV1 extends BaseLoginComponent implements OnInit, OnDe
|
||||
// Save sso params
|
||||
await this.ssoLoginService.setSsoState(state);
|
||||
await this.ssoLoginService.setCodeVerifier(ssoCodeVerifier);
|
||||
|
||||
try {
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(codeChallenge, state);
|
||||
await ipc.platform.localhostCallbackService.openSsoPrompt(codeChallenge, state, email);
|
||||
// FIXME: Remove when updating file. Eslint update
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (err) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user