mirror of
https://github.com/bitwarden/browser
synced 2026-02-02 17:53:41 +00:00
Merge branch 'main' into dirt/pm-26676/risk-insights-refresh-when-org-changes
This commit is contained in:
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -30,7 +30,7 @@ libs/common/src/auth @bitwarden/team-auth-dev
|
||||
apps/browser/src/tools @bitwarden/team-tools-dev
|
||||
apps/cli/src/tools @bitwarden/team-tools-dev
|
||||
apps/desktop/src/app/tools @bitwarden/team-tools-dev
|
||||
apps/desktop/desktop_native/bitwarden_chromium_importer @bitwarden/team-tools-dev
|
||||
apps/desktop/desktop_native/chromium_importer @bitwarden/team-tools-dev
|
||||
apps/web/src/app/tools @bitwarden/team-tools-dev
|
||||
libs/angular/src/tools @bitwarden/team-tools-dev
|
||||
libs/common/src/models/export @bitwarden/team-tools-dev
|
||||
|
||||
54
apps/desktop/desktop_native/Cargo.lock
generated
54
apps/desktop/desktop_native/Cargo.lock
generated
@@ -440,33 +440,6 @@ dependencies = [
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitwarden_chromium_importer"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"cbc",
|
||||
"hex",
|
||||
"homedir",
|
||||
"napi",
|
||||
"napi-derive",
|
||||
"oo7",
|
||||
"pbkdf2",
|
||||
"rand 0.9.1",
|
||||
"rusqlite",
|
||||
"security-framework",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"tokio",
|
||||
"winapi",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -606,6 +579,31 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chromium_importer"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"cbc",
|
||||
"hex",
|
||||
"homedir",
|
||||
"oo7",
|
||||
"pbkdf2",
|
||||
"rand 0.9.1",
|
||||
"rusqlite",
|
||||
"security-framework",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"tokio",
|
||||
"winapi",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@@ -968,7 +966,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"autotype",
|
||||
"base64",
|
||||
"bitwarden_chromium_importer",
|
||||
"chromium_importer",
|
||||
"desktop_core",
|
||||
"hex",
|
||||
"napi",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"autotype",
|
||||
"bitwarden_chromium_importer",
|
||||
"chromium_importer",
|
||||
"core",
|
||||
"macos_provider",
|
||||
"napi",
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
//! Cryptographic primitives used in the SDK
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
|
||||
use aes::cipher::{
|
||||
block_padding::Pkcs7, generic_array::GenericArray, typenum::U32, BlockDecryptMut, KeyIvInit,
|
||||
};
|
||||
|
||||
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.to_vec();
|
||||
cbc::Decryptor::<aes::Aes256>::new(&key, iv)
|
||||
.decrypt_padded_mut::<Pkcs7>(&mut data)
|
||||
.map_err(|_| anyhow!("Failed to decrypt data"))?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use aes::cipher::{
|
||||
generic_array::{sequence::GenericSequence, GenericArray},
|
||||
ArrayLength,
|
||||
};
|
||||
use base64::{engine::general_purpose::STANDARD, Engine};
|
||||
|
||||
pub fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> {
|
||||
(0..length).map(|i| offset + i as u8 * increment).collect()
|
||||
}
|
||||
pub fn generate_generic_array<N: ArrayLength<u8>>(
|
||||
offset: u8,
|
||||
increment: u8,
|
||||
) -> GenericArray<u8, N> {
|
||||
GenericArray::generate(|i| offset + i as u8 * increment)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decrypt_aes256() {
|
||||
let iv = generate_vec(16, 0, 1);
|
||||
let iv: &[u8; 16] = iv.as_slice().try_into().unwrap();
|
||||
let key = generate_generic_array(0, 1);
|
||||
let data: Vec<u8> = STANDARD.decode("ByUF8vhyX4ddU9gcooznwA==").unwrap();
|
||||
|
||||
let decrypted = super::decrypt_aes256(iv, &data, key).unwrap();
|
||||
|
||||
assert_eq!(String::from_utf8(decrypted).unwrap(), "EncryptMe!\u{6}\u{6}\u{6}\u{6}\u{6}\u{6}");
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate napi_derive;
|
||||
|
||||
pub mod chromium;
|
||||
pub mod metadata;
|
||||
pub mod util;
|
||||
|
||||
pub use crate::chromium::platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS;
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "bitwarden_chromium_importer"
|
||||
name = "chromium_importer"
|
||||
edition = { workspace = true }
|
||||
license = { workspace = true }
|
||||
version = { workspace = true }
|
||||
@@ -14,8 +14,6 @@ base64 = { workspace = true }
|
||||
cbc = { workspace = true, features = ["alloc"] }
|
||||
hex = { workspace = true }
|
||||
homedir = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
pbkdf2 = "=0.12.2"
|
||||
rand = { workspace = true }
|
||||
rusqlite = { version = "=0.37.0", features = ["bundled"] }
|
||||
@@ -36,4 +34,3 @@ oo7 = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
# Windows ABE Architecture
|
||||
# Chromium Direct Importer
|
||||
|
||||
## Overview
|
||||
A rust library that allows you to directly import credentials from Chromium-based browsers.
|
||||
|
||||
## Windows ABE Architecture
|
||||
|
||||
On Windows chrome has additional protection measurements which needs to be circumvented in order to
|
||||
get access to the passwords.
|
||||
|
||||
### Overview
|
||||
|
||||
The Windows Application Bound Encryption (ABE) consists of three main components that work together:
|
||||
|
||||
@@ -10,7 +17,7 @@ The Windows Application Bound Encryption (ABE) consists of three main components
|
||||
|
||||
_(The names of the binaries will be changed for the released product.)_
|
||||
|
||||
## The goal
|
||||
### The goal
|
||||
|
||||
The goal of this subsystem is to decrypt the master encryption key with which the login information
|
||||
is encrypted on the local system in Windows. This applies to the most recent versions of Chrome and
|
||||
@@ -24,7 +31,7 @@ Protection API at the system level on top of that. This triply encrypted key is
|
||||
|
||||
The next paragraphs describe what is done at each level to decrypt the key.
|
||||
|
||||
## 1. Client library
|
||||
### 1. Client library
|
||||
|
||||
This is a Rust module that is part of the Chromium importer. It only compiles and runs on Windows
|
||||
(see `abe.rs` and `abe_config.rs`). Its main task is to launch `admin.exe` with elevated privileges
|
||||
@@ -52,7 +59,7 @@ admin.exe --service-exe "c:\temp\service.exe" --encrypted "QVBQQgEAAADQjJ3fARXRE
|
||||
|
||||
**At this point, the user must permit the action to be performed on the UAC screen.**
|
||||
|
||||
## 2. Admin executable
|
||||
### 2. Admin executable
|
||||
|
||||
This executable receives the full path of `service.exe` and the data to be decrypted.
|
||||
|
||||
@@ -67,7 +74,7 @@ is sent to the named pipe server created by the user. The user responds with `ok
|
||||
|
||||
After that, the executable stops and uninstalls the service and then exits.
|
||||
|
||||
## 3. System service
|
||||
### 3. System service
|
||||
|
||||
The service starts and creates a named pipe server for communication between `admin.exe` and the
|
||||
system service. Please note that it is not possible to communicate between the user and the system
|
||||
@@ -83,7 +90,7 @@ removed from the system. Even though we send only one request, the service is de
|
||||
many clients with as many messages as needed and could be installed on the system permanently if
|
||||
necessary.
|
||||
|
||||
## 4. Back to client library
|
||||
### 4. Back to client library
|
||||
|
||||
The decrypted base64-encoded string comes back from the admin executable to the named pipe server at
|
||||
the user level. At this point, it has been decrypted only once at the system level.
|
||||
@@ -99,7 +106,7 @@ itself), it's either AES-256-GCM or ChaCha20Poly1305 encryption scheme. The deta
|
||||
After all of these steps, we have the master key which can be used to decrypt the password
|
||||
information stored in the local database.
|
||||
|
||||
## Summary
|
||||
### Summary
|
||||
|
||||
The Windows ABE decryption process involves a three-tier architecture with named pipe communication:
|
||||
|
||||
@@ -7,11 +7,9 @@ use hex::decode;
|
||||
use homedir::my_home;
|
||||
use rusqlite::{params, Connection};
|
||||
|
||||
// Platform-specific code
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
pub mod platform;
|
||||
mod platform;
|
||||
|
||||
pub(crate) use platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS;
|
||||
|
||||
//
|
||||
// Public API
|
||||
@@ -22,10 +20,7 @@ pub struct ProfileInfo {
|
||||
pub name: String,
|
||||
pub folder: String,
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub account_name: Option<String>,
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub account_email: Option<String>,
|
||||
}
|
||||
|
||||
@@ -113,12 +108,12 @@ pub async fn import_logins(
|
||||
//
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BrowserConfig {
|
||||
pub(crate) struct BrowserConfig {
|
||||
pub name: &'static str,
|
||||
pub data_dir: &'static str,
|
||||
}
|
||||
|
||||
pub static SUPPORTED_BROWSER_MAP: LazyLock<
|
||||
pub(crate) static SUPPORTED_BROWSER_MAP: LazyLock<
|
||||
std::collections::HashMap<&'static str, &'static BrowserConfig>,
|
||||
> = LazyLock::new(|| {
|
||||
platform::SUPPORTED_BROWSERS
|
||||
@@ -140,12 +135,12 @@ fn get_browser_data_dir(config: &BrowserConfig) -> Result<PathBuf> {
|
||||
//
|
||||
|
||||
#[async_trait]
|
||||
pub trait CryptoService: Send {
|
||||
pub(crate) trait CryptoService: Send {
|
||||
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String>;
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone)]
|
||||
pub struct LocalState {
|
||||
pub(crate) struct LocalState {
|
||||
profile: AllProfiles,
|
||||
#[allow(dead_code)]
|
||||
os_crypt: Option<OsCrypt>,
|
||||
@@ -198,16 +193,17 @@ fn load_local_state(browser_dir: &Path) -> Result<LocalState> {
|
||||
}
|
||||
|
||||
fn get_profile_info(local_state: &LocalState) -> Vec<ProfileInfo> {
|
||||
let mut profile_infos = Vec::new();
|
||||
for (name, info) in local_state.profile.info_cache.iter() {
|
||||
profile_infos.push(ProfileInfo {
|
||||
local_state
|
||||
.profile
|
||||
.info_cache
|
||||
.iter()
|
||||
.map(|(name, info)| ProfileInfo {
|
||||
name: info.name.clone(),
|
||||
folder: name.clone(),
|
||||
account_name: info.gaia_name.clone(),
|
||||
account_email: info.user_name.clone(),
|
||||
});
|
||||
}
|
||||
profile_infos
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct EncryptedLogin {
|
||||
@@ -264,17 +260,16 @@ fn hex_to_bytes(hex: &str) -> Vec<u8> {
|
||||
decode(hex).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn does_table_exist(conn: &Connection, table_name: &str) -> Result<bool, rusqlite::Error> {
|
||||
let mut stmt = conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1")?;
|
||||
let exists = stmt.exists(params![table_name])?;
|
||||
Ok(exists)
|
||||
fn table_exist(conn: &Connection, table_name: &str) -> Result<bool, rusqlite::Error> {
|
||||
conn.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1")?
|
||||
.exists(params![table_name])
|
||||
}
|
||||
|
||||
fn query_logins(db_path: &str) -> Result<Vec<EncryptedLogin>, rusqlite::Error> {
|
||||
let conn = Connection::open(db_path)?;
|
||||
|
||||
let have_logins = does_table_exist(&conn, "logins")?;
|
||||
let have_password_notes = does_table_exist(&conn, "password_notes")?;
|
||||
let have_logins = table_exist(&conn, "logins")?;
|
||||
let have_password_notes = table_exist(&conn, "password_notes")?;
|
||||
if !have_logins || !have_password_notes {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
@@ -308,10 +303,7 @@ fn query_logins(db_path: &str) -> Result<Vec<EncryptedLogin>, rusqlite::Error> {
|
||||
})
|
||||
})?;
|
||||
|
||||
let mut logins = Vec::new();
|
||||
for login in logins_iter {
|
||||
logins.push(login?);
|
||||
}
|
||||
let logins = logins_iter.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(logins)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ use crate::util;
|
||||
//
|
||||
|
||||
// TODO: It's possible that there might be multiple possible data directories, depending on the installation method (e.g., snap, flatpak, etc.).
|
||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [
|
||||
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
|
||||
BrowserConfig {
|
||||
name: "Chrome",
|
||||
data_dir: ".config/google-chrome",
|
||||
@@ -32,7 +32,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [
|
||||
},
|
||||
];
|
||||
|
||||
pub fn get_crypto_service(
|
||||
pub(crate) fn get_crypto_service(
|
||||
browser_name: &String,
|
||||
_local_state: &LocalState,
|
||||
) -> Result<Box<dyn CryptoService>> {
|
||||
@@ -10,7 +10,7 @@ use crate::util;
|
||||
// Public API
|
||||
//
|
||||
|
||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 7] = [
|
||||
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
|
||||
BrowserConfig {
|
||||
name: "Chrome",
|
||||
data_dir: "Library/Application Support/Google/Chrome",
|
||||
@@ -41,7 +41,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 7] = [
|
||||
},
|
||||
];
|
||||
|
||||
pub fn get_crypto_service(
|
||||
pub(crate) fn get_crypto_service(
|
||||
browser_name: &String,
|
||||
_local_state: &LocalState,
|
||||
) -> Result<Box<dyn CryptoService>> {
|
||||
@@ -0,0 +1,7 @@
|
||||
// Platform-specific code
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
mod native;
|
||||
|
||||
pub(crate) use native::*;
|
||||
@@ -15,8 +15,7 @@ use crate::util;
|
||||
// Public API
|
||||
//
|
||||
|
||||
// IMPORTANT adjust array size when enabling / disabling chromium importers here
|
||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
|
||||
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
|
||||
BrowserConfig {
|
||||
name: "Brave",
|
||||
data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data",
|
||||
@@ -43,7 +42,7 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
|
||||
},
|
||||
];
|
||||
|
||||
pub fn get_crypto_service(
|
||||
pub(crate) fn get_crypto_service(
|
||||
_browser_name: &str,
|
||||
local_state: &LocalState,
|
||||
) -> Result<Box<dyn CryptoService>> {
|
||||
5
apps/desktop/desktop_native/chromium_importer/src/lib.rs
Normal file
5
apps/desktop/desktop_native/chromium_importer/src/lib.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod chromium;
|
||||
pub mod metadata;
|
||||
mod util;
|
||||
@@ -1,8 +1,7 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{chromium::InstalledBrowserRetriever, PLATFORM_SUPPORTED_BROWSERS};
|
||||
use crate::chromium::{InstalledBrowserRetriever, PLATFORM_SUPPORTED_BROWSERS};
|
||||
|
||||
#[napi(object)]
|
||||
/// Mechanisms that load data into the importer
|
||||
pub struct NativeImporterMetadata {
|
||||
/// Identifies the importer
|
||||
@@ -24,7 +23,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
|
||||
// Check for installed browsers
|
||||
let installed_browsers = T::get_installed_browsers().unwrap_or_default();
|
||||
|
||||
const IMPORTERS: [(&str, &str); 6] = [
|
||||
const IMPORTERS: &[(&str, &str)] = &[
|
||||
("chromecsv", "Chrome"),
|
||||
("chromiumcsv", "Chromium"),
|
||||
("bravecsv", "Brave"),
|
||||
@@ -57,9 +56,7 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
|
||||
map
|
||||
}
|
||||
|
||||
/*
|
||||
Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage
|
||||
*/
|
||||
// Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1,9 +1,6 @@
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
|
||||
use anyhow::{anyhow, Result};
|
||||
use pbkdf2::{hmac::Hmac, pbkdf2};
|
||||
use sha1::Sha1;
|
||||
|
||||
pub fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> {
|
||||
fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> {
|
||||
if encrypted.len() < 3 {
|
||||
return Err(anyhow!(
|
||||
"Corrupted entry: invalid encrypted string length, expected at least 3 bytes, got {}",
|
||||
@@ -15,7 +12,14 @@ pub fn split_encrypted_string(encrypted: &[u8]) -> Result<(&str, &[u8])> {
|
||||
Ok((std::str::from_utf8(version)?, password))
|
||||
}
|
||||
|
||||
pub fn split_encrypted_string_and_validate<'a>(
|
||||
/// A Chromium password consists of three parts:
|
||||
/// - Version (3 bytes): "v10", "v11", etc.
|
||||
/// - Cipher text (chunks of 16 bytes)
|
||||
/// - Padding (1-15 bytes)
|
||||
///
|
||||
/// This function splits the encrypted byte slice into version and cipher text.
|
||||
/// Padding is included and handled by the underlying cryptographic library.
|
||||
pub(crate) fn split_encrypted_string_and_validate<'a>(
|
||||
encrypted: &'a [u8],
|
||||
supported_versions: &[&str],
|
||||
) -> Result<(&'a str, &'a [u8])> {
|
||||
@@ -27,15 +31,22 @@ pub fn split_encrypted_string_and_validate<'a>(
|
||||
Ok((version, password))
|
||||
}
|
||||
|
||||
pub fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
|
||||
let decryptor = cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv)?;
|
||||
let plaintext: Vec<u8> = decryptor
|
||||
/// Decrypt using AES-128 in CBC mode.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos", test))]
|
||||
pub(crate) fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
|
||||
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
|
||||
|
||||
cbc::Decryptor::<aes::Aes128>::new_from_slices(key, iv)?
|
||||
.decrypt_padded_vec_mut::<Pkcs7>(ciphertext)
|
||||
.map_err(|e| anyhow!("Failed to decrypt: {}", e))?;
|
||||
Ok(plaintext)
|
||||
.map_err(|e| anyhow!("Failed to decrypt: {}", e))
|
||||
}
|
||||
|
||||
pub fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
|
||||
/// Derives a PBKDF2 key from the static "saltysalt" salt with the given password and iteration count.
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
pub(crate) fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
|
||||
use pbkdf2::{hmac::Hmac, pbkdf2};
|
||||
use sha1::Sha1;
|
||||
|
||||
let mut key = vec![0u8; 16];
|
||||
pbkdf2::<Hmac<Sha1>>(password, b"saltysalt", iterations, &mut key)
|
||||
.map_err(|e| anyhow!("Failed to derive master key: {}", e))?;
|
||||
@@ -44,16 +55,6 @@ pub fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
pub fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> {
|
||||
(0..length).map(|i| offset + i as u8 * increment).collect()
|
||||
}
|
||||
pub fn generate_generic_array<N: ArrayLength<u8>>(
|
||||
offset: u8,
|
||||
increment: u8,
|
||||
) -> GenericArray<u8, N> {
|
||||
GenericArray::generate(|i| offset + i as u8 * increment)
|
||||
}
|
||||
|
||||
use aes::cipher::{
|
||||
block_padding::Pkcs7,
|
||||
generic_array::{sequence::GenericSequence, GenericArray},
|
||||
@@ -64,6 +65,17 @@ mod tests {
|
||||
const LENGTH10: usize = 10;
|
||||
const LENGTH0: usize = 0;
|
||||
|
||||
fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> {
|
||||
(0..length).map(|i| offset + i as u8 * increment).collect()
|
||||
}
|
||||
|
||||
fn generate_generic_array<N: ArrayLength<u8>>(
|
||||
offset: u8,
|
||||
increment: u8,
|
||||
) -> GenericArray<u8, N> {
|
||||
GenericArray::generate(|i| offset + i as u8 * increment)
|
||||
}
|
||||
|
||||
fn run_split_encrypted_string_test<'a, const N: usize>(
|
||||
successfully_split: bool,
|
||||
plaintext_to_encrypt: &'a str,
|
||||
@@ -17,7 +17,7 @@ manual_test = []
|
||||
anyhow = { workspace = true }
|
||||
autotype = { path = "../autotype" }
|
||||
base64 = { workspace = true }
|
||||
bitwarden_chromium_importer = { path = "../bitwarden_chromium_importer" }
|
||||
chromium_importer = { path = "../chromium_importer" }
|
||||
desktop_core = { path = "../core" }
|
||||
hex = { workspace = true }
|
||||
napi = { workspace = true, features = ["async"] }
|
||||
|
||||
14
apps/desktop/desktop_native/napi/index.d.ts
vendored
14
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -3,15 +3,6 @@
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
/** Mechanisms that load data into the importer */
|
||||
export interface NativeImporterMetadata {
|
||||
/** Identifies the importer */
|
||||
id: string
|
||||
/** Describes the strategies used to obtain imported data */
|
||||
loaders: Array<string>
|
||||
/** Identifies the instructions for the importer */
|
||||
instructions: string
|
||||
}
|
||||
export declare namespace passwords {
|
||||
/** The error message returned when a password is not found during retrieval or deletion. */
|
||||
export const PASSWORD_NOT_FOUND: string
|
||||
@@ -249,6 +240,11 @@ export declare namespace chromium_importer {
|
||||
login?: Login
|
||||
failure?: LoginImportFailure
|
||||
}
|
||||
export interface NativeImporterMetadata {
|
||||
id: string
|
||||
loaders: Array<string>
|
||||
instructions: string
|
||||
}
|
||||
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
|
||||
export function getMetadata(): Record<string, NativeImporterMetadata>
|
||||
export function getInstalledBrowsers(): Array<string>
|
||||
|
||||
@@ -1064,11 +1064,13 @@ pub mod logging {
|
||||
|
||||
#[napi]
|
||||
pub mod chromium_importer {
|
||||
use bitwarden_chromium_importer::chromium::DefaultInstalledBrowserRetriever;
|
||||
use bitwarden_chromium_importer::chromium::InstalledBrowserRetriever;
|
||||
use bitwarden_chromium_importer::chromium::LoginImportResult as _LoginImportResult;
|
||||
use bitwarden_chromium_importer::chromium::ProfileInfo as _ProfileInfo;
|
||||
use bitwarden_chromium_importer::metadata::NativeImporterMetadata;
|
||||
use chromium_importer::{
|
||||
chromium::{
|
||||
DefaultInstalledBrowserRetriever, InstalledBrowserRetriever,
|
||||
LoginImportResult as _LoginImportResult, ProfileInfo as _ProfileInfo,
|
||||
},
|
||||
metadata::NativeImporterMetadata as _NativeImporterMetadata,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[napi(object)]
|
||||
@@ -1098,6 +1100,13 @@ pub mod chromium_importer {
|
||||
pub failure: Option<LoginImportFailure>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct NativeImporterMetadata {
|
||||
pub id: String,
|
||||
pub loaders: Vec<&'static str>,
|
||||
pub instructions: &'static str,
|
||||
}
|
||||
|
||||
impl From<_LoginImportResult> for LoginImportResult {
|
||||
fn from(l: _LoginImportResult) -> Self {
|
||||
match l {
|
||||
@@ -1131,23 +1140,34 @@ pub mod chromium_importer {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<_NativeImporterMetadata> for NativeImporterMetadata {
|
||||
fn from(m: _NativeImporterMetadata) -> Self {
|
||||
NativeImporterMetadata {
|
||||
id: m.id,
|
||||
loaders: m.loaders,
|
||||
instructions: m.instructions,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
/// Returns OS aware metadata describing supported Chromium based importers as a JSON string.
|
||||
pub fn get_metadata() -> HashMap<String, NativeImporterMetadata> {
|
||||
bitwarden_chromium_importer::metadata::get_supported_importers::<
|
||||
DefaultInstalledBrowserRetriever,
|
||||
>()
|
||||
chromium_importer::metadata::get_supported_importers::<DefaultInstalledBrowserRetriever>()
|
||||
.into_iter()
|
||||
.map(|(browser, metadata)| (browser, NativeImporterMetadata::from(metadata)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_installed_browsers() -> napi::Result<Vec<String>> {
|
||||
bitwarden_chromium_importer::chromium::DefaultInstalledBrowserRetriever::get_installed_browsers()
|
||||
chromium_importer::chromium::DefaultInstalledBrowserRetriever::get_installed_browsers()
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_available_profiles(browser: String) -> napi::Result<Vec<ProfileInfo>> {
|
||||
bitwarden_chromium_importer::chromium::get_available_profiles(&browser)
|
||||
chromium_importer::chromium::get_available_profiles(&browser)
|
||||
.map(|profiles| profiles.into_iter().map(ProfileInfo::from).collect())
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
@@ -1157,7 +1177,7 @@ pub mod chromium_importer {
|
||||
browser: String,
|
||||
profile_id: String,
|
||||
) -> napi::Result<Vec<LoginImportResult>> {
|
||||
bitwarden_chromium_importer::chromium::import_logins(&browser, &profile_id)
|
||||
chromium_importer::chromium::import_logins(&browser, &profile_id)
|
||||
.await
|
||||
.map(|logins| logins.into_iter().map(LoginImportResult::from).collect())
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import type { NativeImporterMetadata } from "@bitwarden/desktop-napi";
|
||||
import type { chromium_importer } from "@bitwarden/desktop-napi";
|
||||
import {
|
||||
ImportType,
|
||||
DefaultImportMetadataService,
|
||||
@@ -25,7 +25,9 @@ export class DesktopImportMetadataService
|
||||
await super.init();
|
||||
}
|
||||
|
||||
private async parseNativeMetaData(raw: Record<string, NativeImporterMetadata>): Promise<void> {
|
||||
private async parseNativeMetaData(
|
||||
raw: Record<string, chromium_importer.NativeImporterMetadata>,
|
||||
): Promise<void> {
|
||||
const entries = Object.entries(raw).map(([id, meta]) => {
|
||||
const loaders = meta.loaders.map(this.mapLoader);
|
||||
const instructions = this.mapInstructions(meta.instructions);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
import type { NativeImporterMetadata } from "@bitwarden/desktop-napi";
|
||||
import type { chromium_importer } from "@bitwarden/desktop-napi";
|
||||
|
||||
const chromiumImporter = {
|
||||
getMetadata: (): Promise<Record<string, NativeImporterMetadata>> =>
|
||||
getMetadata: (): Promise<Record<string, chromium_importer.NativeImporterMetadata>> =>
|
||||
ipcRenderer.invoke("chromium_importer.getMetadata"),
|
||||
getInstalledBrowsers: (): Promise<string[]> =>
|
||||
ipcRenderer.invoke("chromium_importer.getInstalledBrowsers"),
|
||||
|
||||
@@ -31,6 +31,7 @@ import { ProviderOrganizationCreateRequest } from "@bitwarden/common/admin-conso
|
||||
import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { PlanSponsorshipType, PlanType, ProductTierType } from "@bitwarden/common/billing/enums";
|
||||
import { BillingResponse } from "@bitwarden/common/billing/models/response/billing.response";
|
||||
import { OrganizationSubscriptionResponse } from "@bitwarden/common/billing/models/response/organization-subscription.response";
|
||||
@@ -41,7 +42,7 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { ToastService } from "@bitwarden/components";
|
||||
@@ -654,7 +655,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
|
||||
orgId = this.selfHosted
|
||||
? await this.createSelfHosted(key, collectionCt, orgKeys)
|
||||
: await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
|
||||
: await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1], activeUserId);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -808,6 +809,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
collectionCt: string,
|
||||
orgKeys: [string, EncString],
|
||||
orgKey: SymmetricCryptoKey,
|
||||
activeUserId: UserId,
|
||||
): Promise<string> {
|
||||
const request = new OrganizationCreateRequest();
|
||||
request.key = key;
|
||||
@@ -855,7 +857,14 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||
this.formGroup.controls.clientOwnerEmail.value,
|
||||
request,
|
||||
);
|
||||
const providerKey = await this.keyService.getProviderKey(this.providerId);
|
||||
|
||||
const providerKey = await firstValueFrom(
|
||||
this.keyService
|
||||
.providerKeys$(activeUserId)
|
||||
.pipe(map((providerKeys) => providerKeys?.[this.providerId as ProviderId] ?? null)),
|
||||
);
|
||||
assertNonNullish(providerKey, "Provider key not found");
|
||||
|
||||
providerRequest.organizationCreateRequest.key = (
|
||||
await this.encryptService.wrapSymmetricKey(orgKey, providerKey)
|
||||
).encryptedString;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { AddableOrganizationResponse } from "@bitwarden/common/admin-console/models/response/addable-organization.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import {
|
||||
DIALOG_DATA,
|
||||
@@ -46,6 +49,7 @@ export class AddExistingOrganizationDialogComponent implements OnInit {
|
||||
private providerApiService: ProviderApiServiceAbstraction,
|
||||
private toastService: ToastService,
|
||||
private webProviderService: WebProviderService,
|
||||
private accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -57,9 +61,11 @@ export class AddExistingOrganizationDialogComponent implements OnInit {
|
||||
|
||||
addExistingOrganization = async (): Promise<void> => {
|
||||
if (this.selectedOrganization) {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
await this.webProviderService.addOrganizationToProvider(
|
||||
this.dialogParams.provider.id,
|
||||
this.selectedOrganization.id,
|
||||
userId,
|
||||
);
|
||||
|
||||
this.toastService.showToast({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, Inject } from "@angular/core";
|
||||
import { firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import {
|
||||
OrganizationUserBulkPublicKeyResponse,
|
||||
@@ -12,10 +13,14 @@ import { ProviderUserBulkConfirmRequest } from "@bitwarden/common/admin-console/
|
||||
import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request";
|
||||
import { ProviderUserBulkPublicKeyResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk-public-key.response";
|
||||
import { ProviderUserBulkResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user-bulk.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { ProviderId } from "@bitwarden/common/types/guid";
|
||||
import { ProviderKey } from "@bitwarden/common/types/key";
|
||||
import { DIALOG_DATA, DialogConfig, DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { BaseBulkConfirmComponent } from "@bitwarden/web-vault/app/admin-console/organizations/members/components/bulk/base-bulk-confirm.component";
|
||||
@@ -35,6 +40,7 @@ type BulkConfirmDialogParams = {
|
||||
})
|
||||
export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||
providerId: string;
|
||||
providerKey$: Observable<ProviderKey>;
|
||||
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
@@ -42,15 +48,21 @@ export class BulkConfirmDialogComponent extends BaseBulkConfirmComponent {
|
||||
protected encryptService: EncryptService,
|
||||
@Inject(DIALOG_DATA) protected dialogParams: BulkConfirmDialogParams,
|
||||
protected i18nService: I18nService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
super(keyService, encryptService, i18nService);
|
||||
|
||||
this.providerId = dialogParams.providerId;
|
||||
this.providerKey$ = this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.providerKeys$(userId)),
|
||||
map((providerKeysById) => providerKeysById?.[this.providerId as ProviderId]),
|
||||
);
|
||||
this.users = dialogParams.users;
|
||||
}
|
||||
|
||||
protected getCryptoKey = (): Promise<SymmetricCryptoKey> =>
|
||||
this.keyService.getProviderKey(this.providerId);
|
||||
protected getCryptoKey = async (): Promise<SymmetricCryptoKey> =>
|
||||
await firstValueFrom(this.providerKey$);
|
||||
|
||||
protected getPublicKeys = async (): Promise<
|
||||
ListResponse<OrganizationUserBulkPublicKeyResponse | ProviderUserBulkPublicKeyResponse>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { combineLatest, firstValueFrom, lastValueFrom, switchMap } from "rxjs";
|
||||
import { first } from "rxjs/operators";
|
||||
import { first, map } from "rxjs/operators";
|
||||
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -16,11 +16,13 @@ import { ProviderUserConfirmRequest } from "@bitwarden/common/admin-console/mode
|
||||
import { ProviderUserUserDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-user.response";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
import { ProviderId } from "@bitwarden/common/types/guid";
|
||||
import { DialogRef, DialogService, ToastService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { BaseMembersComponent } from "@bitwarden/web-vault/app/admin-console/common/base-members.component";
|
||||
@@ -204,7 +206,15 @@ export class MembersComponent extends BaseMembersComponent<ProviderUser> {
|
||||
|
||||
async confirmUser(user: ProviderUser, publicKey: Uint8Array): Promise<MemberActionResult> {
|
||||
try {
|
||||
const providerKey = await this.keyService.getProviderKey(this.providerId);
|
||||
const providerKey = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(
|
||||
getUserId,
|
||||
switchMap((userId) => this.keyService.providerKeys$(userId)),
|
||||
map((providerKeys) => providerKeys?.[this.providerId as ProviderId] ?? null),
|
||||
),
|
||||
);
|
||||
assertNonNullish(providerKey, "Provider key not found");
|
||||
|
||||
const key = await this.encryptService.encapsulateKeyUnsigned(providerKey, publicKey);
|
||||
const request = new ProviderUserConfirmRequest();
|
||||
request.key = key.encryptedString;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
@@ -8,7 +9,6 @@ import { EncryptService } from "@bitwarden/common/key-management/crypto/abstract
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { OrgKey, ProviderKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
@@ -24,16 +24,22 @@ describe("WebProviderService", () => {
|
||||
let apiService: MockProxy<ApiService>;
|
||||
let i18nService: MockProxy<I18nService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let stateProvider: MockProxy<StateProvider>;
|
||||
let providerApiService: MockProxy<ProviderApiServiceAbstraction>;
|
||||
|
||||
const activeUserId = newGuid() as UserId;
|
||||
const providerId = "provider-123";
|
||||
const mockOrgKey = new SymmetricCryptoKey(new Uint8Array(64)) as OrgKey;
|
||||
const mockProviderKey = new SymmetricCryptoKey(new Uint8Array(64)) as ProviderKey;
|
||||
const mockProviderKeysById: Record<string, ProviderKey> = {
|
||||
[providerId]: mockProviderKey,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
keyService = mock();
|
||||
syncService = mock();
|
||||
apiService = mock();
|
||||
i18nService = mock();
|
||||
encryptService = mock();
|
||||
stateProvider = mock();
|
||||
providerApiService = mock();
|
||||
|
||||
sut = new WebProviderService(
|
||||
@@ -42,14 +48,69 @@ describe("WebProviderService", () => {
|
||||
apiService,
|
||||
i18nService,
|
||||
encryptService,
|
||||
stateProvider,
|
||||
providerApiService,
|
||||
);
|
||||
});
|
||||
|
||||
describe("addOrganizationToProvider", () => {
|
||||
const organizationId = "org-789";
|
||||
const encryptedOrgKey = new EncString("encrypted-org-key");
|
||||
const mockOrgKeysById: Record<string, OrgKey> = {
|
||||
[organizationId]: mockOrgKey,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
keyService.orgKeys$.mockReturnValue(of(mockOrgKeysById));
|
||||
keyService.providerKeys$.mockReturnValue(of(mockProviderKeysById));
|
||||
encryptService.wrapSymmetricKey.mockResolvedValue(encryptedOrgKey);
|
||||
});
|
||||
|
||||
it("adds an organization to a provider with correct encryption", async () => {
|
||||
await sut.addOrganizationToProvider(providerId, organizationId, activeUserId);
|
||||
|
||||
expect(keyService.orgKeys$).toHaveBeenCalledWith(activeUserId);
|
||||
expect(keyService.providerKeys$).toHaveBeenCalledWith(activeUserId);
|
||||
expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey);
|
||||
expect(providerApiService.addOrganizationToProvider).toHaveBeenCalledWith(providerId, {
|
||||
key: encryptedOrgKey.encryptedString,
|
||||
organizationId,
|
||||
});
|
||||
expect(syncService.fullSync).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("throws an error if organization key is not found", async () => {
|
||||
const invalidOrgId = "invalid-org";
|
||||
|
||||
await expect(
|
||||
sut.addOrganizationToProvider(providerId, invalidOrgId, activeUserId),
|
||||
).rejects.toThrow("Organization key not found");
|
||||
});
|
||||
|
||||
it("throws an error if no organization keys are available", async () => {
|
||||
keyService.orgKeys$.mockReturnValue(of(null));
|
||||
|
||||
await expect(
|
||||
sut.addOrganizationToProvider(providerId, organizationId, activeUserId),
|
||||
).rejects.toThrow("Organization key not found");
|
||||
});
|
||||
|
||||
it("throws an error if provider key is not found", async () => {
|
||||
const invalidProviderId = "invalid-provider";
|
||||
await expect(
|
||||
sut.addOrganizationToProvider(invalidProviderId, organizationId, activeUserId),
|
||||
).rejects.toThrow("Provider key not found");
|
||||
});
|
||||
|
||||
it("throws an error if no provider keys are available", async () => {
|
||||
keyService.providerKeys$.mockReturnValue(of(null));
|
||||
|
||||
await expect(
|
||||
sut.addOrganizationToProvider(providerId, organizationId, activeUserId),
|
||||
).rejects.toThrow("Provider key not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createClientOrganization", () => {
|
||||
const activeUserId = newGuid() as UserId;
|
||||
const providerId = "provider-123";
|
||||
const name = "Test Org";
|
||||
const ownerEmail = "owner@example.com";
|
||||
const planType = PlanType.EnterpriseAnnually;
|
||||
@@ -59,15 +120,13 @@ describe("WebProviderService", () => {
|
||||
const encryptedProviderKey = new EncString("encrypted-provider-key");
|
||||
const encryptedCollectionName = new EncString("encrypted-collection-name");
|
||||
const defaultCollectionTranslation = "Default Collection";
|
||||
const mockOrgKey = new SymmetricCryptoKey(new Uint8Array(64)) as OrgKey;
|
||||
const mockProviderKey = new SymmetricCryptoKey(new Uint8Array(64)) as ProviderKey;
|
||||
|
||||
beforeEach(() => {
|
||||
keyService.makeOrgKey.mockResolvedValue([new EncString("mockEncryptedKey"), mockOrgKey]);
|
||||
keyService.makeKeyPair.mockResolvedValue([publicKey, encryptedPrivateKey]);
|
||||
i18nService.t.mockReturnValue(defaultCollectionTranslation);
|
||||
encryptService.encryptString.mockResolvedValue(encryptedCollectionName);
|
||||
keyService.getProviderKey.mockResolvedValue(mockProviderKey);
|
||||
keyService.providerKeys$.mockReturnValue(of(mockProviderKeysById));
|
||||
encryptService.wrapSymmetricKey.mockResolvedValue(encryptedProviderKey);
|
||||
});
|
||||
|
||||
@@ -88,7 +147,7 @@ describe("WebProviderService", () => {
|
||||
defaultCollectionTranslation,
|
||||
mockOrgKey,
|
||||
);
|
||||
expect(keyService.getProviderKey).toHaveBeenCalledWith(providerId);
|
||||
expect(keyService.providerKeys$).toHaveBeenCalledWith(activeUserId);
|
||||
expect(encryptService.wrapSymmetricKey).toHaveBeenCalledWith(mockOrgKey, mockProviderKey);
|
||||
|
||||
expect(providerApiService.createProviderOrganization).toHaveBeenCalledWith(
|
||||
@@ -107,5 +166,27 @@ describe("WebProviderService", () => {
|
||||
expect(apiService.refreshIdentityToken).toHaveBeenCalled();
|
||||
expect(syncService.fullSync).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it("throws an error if provider key is not found", async () => {
|
||||
const invalidProviderId = "invalid-provider";
|
||||
await expect(
|
||||
sut.createClientOrganization(
|
||||
invalidProviderId,
|
||||
name,
|
||||
ownerEmail,
|
||||
planType,
|
||||
seats,
|
||||
activeUserId,
|
||||
),
|
||||
).rejects.toThrow("Provider key not found");
|
||||
});
|
||||
|
||||
it("throws an error if no provider keys are available", async () => {
|
||||
keyService.providerKeys$.mockReturnValue(of(null));
|
||||
|
||||
await expect(
|
||||
sut.createClientOrganization(providerId, name, ownerEmail, planType, seats, activeUserId),
|
||||
).rejects.toThrow("Provider key not found");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Injectable } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { switchMap } from "rxjs/operators";
|
||||
import { combineLatest, firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction";
|
||||
import { CreateProviderOrganizationRequest } from "@bitwarden/common/admin-console/models/request/create-provider-organization.request";
|
||||
import { OrganizationKeysRequest } from "@bitwarden/common/admin-console/models/request/organization-keys.request";
|
||||
import { assertNonNullish } from "@bitwarden/common/auth/utils";
|
||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrgKey } from "@bitwarden/common/types/key";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
@@ -25,18 +24,26 @@ export class WebProviderService {
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
private encryptService: EncryptService,
|
||||
private stateProvider: StateProvider,
|
||||
private providerApiService: ProviderApiServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async addOrganizationToProvider(providerId: string, organizationId: string): Promise<void> {
|
||||
const orgKey = await firstValueFrom(
|
||||
this.stateProvider.activeUserId$.pipe(
|
||||
switchMap((userId) => this.keyService.orgKeys$(userId)),
|
||||
map((organizationKeysById) => organizationKeysById[organizationId as OrganizationId]),
|
||||
),
|
||||
async addOrganizationToProvider(
|
||||
providerId: string,
|
||||
organizationId: string,
|
||||
activeUserId: UserId,
|
||||
): Promise<void> {
|
||||
const [orgKeysById, providerKeys] = await firstValueFrom(
|
||||
combineLatest([
|
||||
this.keyService.orgKeys$(activeUserId),
|
||||
this.keyService.providerKeys$(activeUserId),
|
||||
]),
|
||||
);
|
||||
const providerKey = await this.keyService.getProviderKey(providerId);
|
||||
|
||||
const orgKey = orgKeysById?.[organizationId as OrganizationId];
|
||||
const providerKey = providerKeys?.[providerId as ProviderId];
|
||||
assertNonNullish(orgKey, "Organization key not found");
|
||||
assertNonNullish(providerKey, "Provider key not found");
|
||||
|
||||
const encryptedOrgKey = await this.encryptService.wrapSymmetricKey(orgKey, providerKey);
|
||||
await this.providerApiService.addOrganizationToProvider(providerId, {
|
||||
key: encryptedOrgKey.encryptedString,
|
||||
@@ -62,7 +69,12 @@ export class WebProviderService {
|
||||
organizationKey,
|
||||
);
|
||||
|
||||
const providerKey = await this.keyService.getProviderKey(providerId);
|
||||
const providerKey = await firstValueFrom(
|
||||
this.keyService
|
||||
.providerKeys$(activeUserId)
|
||||
.pipe(map((providerKeys) => providerKeys?.[providerId as ProviderId])),
|
||||
);
|
||||
assertNonNullish(providerKey, "Provider key not found");
|
||||
|
||||
const encryptedProviderKey = await this.encryptService.wrapSymmetricKey(
|
||||
organizationKey,
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
<ng-container *ngTemplateOutlet="prefixContent"></ng-container>
|
||||
</div>
|
||||
<div
|
||||
class="tw-w-full tw-pb-0 tw-relative [&>*]:tw-p-0 [&>*::selection]:tw-bg-primary-700 [&>*::selection]:tw-text-contrast"
|
||||
class="tw-w-full tw-min-w-0 tw-pb-0 tw-relative [&>*]:tw-p-0 [&>*::selection]:tw-bg-primary-700 [&>*::selection]:tw-text-contrast"
|
||||
data-default-content
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="defaultContent"></ng-container>
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { WrappedSigningKey } from "@bitwarden/common/key-management/types";
|
||||
import { KeySuffixOptions, HashPurpose } from "@bitwarden/common/platform/enums";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
UserKey,
|
||||
MasterKey,
|
||||
@@ -248,17 +248,19 @@ export abstract class KeyService {
|
||||
|
||||
/**
|
||||
* Stores the provider keys for a given user.
|
||||
* @param orgs The provider orgs for which to save the keys from.
|
||||
* @param providers The provider orgs for which to save the keys from.
|
||||
* @param userId The user id of the user for which to store the keys for.
|
||||
*/
|
||||
abstract setProviderKeys(orgs: ProfileProviderResponse[], userId: UserId): Promise<void>;
|
||||
abstract setProviderKeys(providers: ProfileProviderResponse[], userId: UserId): Promise<void>;
|
||||
|
||||
/**
|
||||
*
|
||||
* @throws Error when providerId is null or no active user
|
||||
* @param providerId The desired provider
|
||||
* @returns The provider's symmetric key
|
||||
* Gets an observable of provider keys for the given user.
|
||||
* @param userId The user to get provider keys for.
|
||||
* @return An observable stream of the users providers keys if they are unlocked, or null if the user is not unlocked.
|
||||
* @throws If an invalid user id is passed in.
|
||||
*/
|
||||
abstract getProviderKey(providerId: string): Promise<ProviderKey | null>;
|
||||
abstract providerKeys$(userId: UserId): Observable<Record<ProviderId, ProviderKey> | null>;
|
||||
|
||||
/**
|
||||
* Creates a new organization key and encrypts it with the user's public key.
|
||||
* This method can also return Provider keys for creating new Provider users.
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
FakeSingleUserState,
|
||||
} from "@bitwarden/common/spec";
|
||||
import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { OrganizationId, ProviderId, UserId } from "@bitwarden/common/types/guid";
|
||||
import {
|
||||
UserKey,
|
||||
MasterKey,
|
||||
@@ -1314,6 +1314,49 @@ describe("keyService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("providerKeys$", () => {
|
||||
let mockUserPrivateKey: Uint8Array;
|
||||
let mockProviderKeys: Record<ProviderId, ProviderKey>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUserPrivateKey = makeStaticByteArray(64, 1);
|
||||
mockProviderKeys = {
|
||||
["provider1" as ProviderId]: makeSymmetricCryptoKey<ProviderKey>(64),
|
||||
["provider2" as ProviderId]: makeSymmetricCryptoKey<ProviderKey>(64),
|
||||
};
|
||||
});
|
||||
|
||||
it("returns null when userPrivateKey is null", async () => {
|
||||
jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(null));
|
||||
|
||||
const result = await firstValueFrom(keyService.providerKeys$(mockUserId));
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns provider keys when userPrivateKey is available", async () => {
|
||||
jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(mockUserPrivateKey as any));
|
||||
jest.spyOn(keyService as any, "providerKeysHelper$").mockReturnValue(of(mockProviderKeys));
|
||||
|
||||
const result = await firstValueFrom(keyService.providerKeys$(mockUserId));
|
||||
|
||||
expect(result).toEqual(mockProviderKeys);
|
||||
expect((keyService as any).providerKeysHelper$).toHaveBeenCalledWith(
|
||||
mockUserId,
|
||||
mockUserPrivateKey,
|
||||
);
|
||||
});
|
||||
|
||||
it("returns null when providerKeysHelper$ returns null", async () => {
|
||||
jest.spyOn(keyService, "userPrivateKey$").mockReturnValue(of(mockUserPrivateKey as any));
|
||||
jest.spyOn(keyService as any, "providerKeysHelper$").mockReturnValue(of(null));
|
||||
|
||||
const result = await firstValueFrom(keyService.providerKeys$(mockUserId));
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("makeKeyPair", () => {
|
||||
test.each([null as unknown as SymmetricCryptoKey, undefined as unknown as SymmetricCryptoKey])(
|
||||
"throws when the provided key is %s",
|
||||
|
||||
@@ -426,20 +426,16 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Deprecate in favor of observable
|
||||
async getProviderKey(providerId: ProviderId): Promise<ProviderKey | null> {
|
||||
if (providerId == null) {
|
||||
return null;
|
||||
}
|
||||
providerKeys$(userId: UserId): Observable<Record<ProviderId, ProviderKey> | null> {
|
||||
return this.userPrivateKey$(userId).pipe(
|
||||
switchMap((userPrivateKey) => {
|
||||
if (userPrivateKey == null) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
if (activeUserId == null) {
|
||||
throw new Error("No active user found.");
|
||||
}
|
||||
|
||||
const providerKeys = await firstValueFrom(this.providerKeys$(activeUserId));
|
||||
|
||||
return providerKeys?.[providerId] ?? null;
|
||||
return this.providerKeysHelper$(userId, userPrivateKey);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async clearProviderKeys(userId: UserId): Promise<void> {
|
||||
@@ -829,18 +825,6 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
)) as UserPrivateKey;
|
||||
}
|
||||
|
||||
providerKeys$(userId: UserId) {
|
||||
return this.userPrivateKey$(userId).pipe(
|
||||
switchMap((userPrivateKey) => {
|
||||
if (userPrivateKey == null) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.providerKeysHelper$(userId, userPrivateKey);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper for decrypting provider keys that requires a user id and that users decrypted private key
|
||||
* this is helpful for when you may have already grabbed the user private key and don't want to redo
|
||||
|
||||
Reference in New Issue
Block a user