1
0
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:
voommen-livefront
2025-10-27 12:09:04 -05:00
30 changed files with 385 additions and 250 deletions

2
.github/CODEOWNERS vendored
View File

@@ -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

View File

@@ -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",

View File

@@ -2,7 +2,7 @@
resolver = "2"
members = [
"autotype",
"bitwarden_chromium_importer",
"chromium_importer",
"core",
"macos_provider",
"napi",

View File

@@ -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}");
}
}

View File

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

View File

@@ -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

View File

@@ -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:

View File

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

View File

@@ -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>> {

View File

@@ -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>> {

View File

@@ -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::*;

View File

@@ -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>> {

View File

@@ -0,0 +1,5 @@
#![doc = include_str!("../README.md")]
pub mod chromium;
pub mod metadata;
mod util;

View File

@@ -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::*;

View File

@@ -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,

View File

@@ -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"] }

View File

@@ -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>

View File

@@ -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()))

View File

@@ -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);

View File

@@ -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"),

View File

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

View File

@@ -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({

View File

@@ -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>

View File

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

View File

@@ -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");
});
});
});

View File

@@ -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,

View File

@@ -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>

View File

@@ -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.

View File

@@ -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",

View File

@@ -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