1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 13:10:17 +00:00

Merge branch 'main' into ac/pm-19814/improved-warning-popup-ui

This commit is contained in:
Jimmy Vo
2025-04-21 11:22:23 -04:00
94 changed files with 911 additions and 582 deletions

3
.github/CODEOWNERS vendored
View File

@@ -8,7 +8,8 @@
apps/desktop/desktop_native @bitwarden/team-platform-dev
apps/desktop/desktop_native/objc/src/native/autofill @bitwarden/team-autofill-dev
apps/desktop/desktop_native/core/src/autofill @bitwarden/team-autofill-dev
## No ownership for Cargo.toml to allow dependency updates
## No ownership fo Cargo.lock and Cargo.toml to allow dependency updates
apps/desktop/desktop_native/Cargo.lock
apps/desktop/desktop_native/Cargo.toml
## Auth team files ##

View File

@@ -49,6 +49,7 @@
"./github/workflows/release-web.yml",
],
commitMessagePrefix: "[deps] BRE:",
addLabels: ["hold"],
},
{
// Disable major and minor updates for TypeScript and Zone.js because they are managed by Angular.

View File

@@ -12,12 +12,13 @@ on:
- 'cf-pages'
paths:
- 'apps/cli/**'
- 'bitwarden_license/bit-cli/**'
- 'bitwarden_license/bit-common/**'
- 'libs/**'
- '*'
- '!*.md'
- '!*.txt'
- '.github/workflows/build-cli.yml'
- 'bitwarden_license/bit-cli/**'
push:
branches:
- 'main'
@@ -25,12 +26,13 @@ on:
- 'hotfix-rc-cli'
paths:
- 'apps/cli/**'
- 'bitwarden_license/bit-cli/**'
- 'bitwarden_license/bit-common/**'
- 'libs/**'
- '*'
- '!*.md'
- '!*.txt'
- '.github/workflows/build-cli.yml'
- 'bitwarden_license/bit-cli/**'
workflow_call:
inputs: {}
workflow_dispatch:

View File

@@ -12,6 +12,8 @@ on:
- 'cf-pages'
paths:
- 'apps/web/**'
- 'bitwarden_license/bit-common/**'
- 'bitwarden_license/bit-web/**'
- 'libs/**'
- '*'
- '!*.md'
@@ -24,6 +26,8 @@ on:
- 'hotfix-rc-web'
paths:
- 'apps/web/**'
- 'bitwarden_license/bit-common/**'
- 'bitwarden_license/bit-web/**'
- 'libs/**'
- '*'
- '!*.md'

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/browser",
"version": "2025.3.2",
"version": "2025.4.0",
"scripts": {
"build": "npm run build:chrome",
"build:chrome": "cross-env BROWSER=chrome MANIFEST_VERSION=3 NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",

View File

@@ -2,6 +2,9 @@
"appName": {
"message": "Bitwarden"
},
"appLogoLabel": {
"message": "Bitwarden logo"
},
"extName": {
"message": "Bitwarden Password Manager",
"description": "Extension name, MUST be less than 40 characters (Safari restriction)"

View File

@@ -5,7 +5,12 @@
[showBackButton]="showBackButton"
[pageTitle]="''"
>
<bit-icon *ngIf="showLogo" class="tw-inline-flex" [icon]="logo"></bit-icon>
<bit-icon
*ngIf="showLogo"
class="tw-inline-flex"
[icon]="logo"
[ariaLabel]="'appLogoLabel' | i18n"
></bit-icon>
<ng-container slot="end">
<app-pop-out></app-pop-out>

View File

@@ -12,6 +12,7 @@ import {
} from "@bitwarden/auth/angular";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Icon, IconModule, Translation } from "@bitwarden/components";
import { I18nPipe } from "@bitwarden/ui-common";
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
@@ -36,6 +37,7 @@ export interface ExtensionAnonLayoutWrapperData extends AnonLayoutWrapperData {
AnonLayoutComponent,
CommonModule,
CurrentAccountComponent,
I18nPipe,
IconModule,
PopOutComponent,
PopupPageComponent,

View File

@@ -1,6 +1,6 @@
import { Component } from "@angular/core";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
@Component({
selector: "app-remove-password",

View File

@@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "__MSG_extName__",
"short_name": "Bitwarden",
"version": "2025.3.2",
"version": "2025.4.0",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",

View File

@@ -3,7 +3,7 @@
"minimum_chrome_version": "102.0",
"name": "__MSG_extName__",
"short_name": "Bitwarden",
"version": "2025.3.2",
"version": "2025.4.0",
"description": "__MSG_extDesc__",
"default_locale": "en",
"author": "Bitwarden Inc.",

View File

@@ -64,10 +64,6 @@ import { BlockedDomainsComponent } from "../autofill/popup/settings/blocked-doma
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { PremiumV2Component } from "../billing/popup/settings/premium-v2.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { PhishingLandingIcon } from "../phishing-detection/images/phishing-landing-icon";
import { LearnMoreComponent } from "../phishing-detection/pages/learn-more-component";
import { PhishingWarning } from "../phishing-detection/pages/phishing-warning";
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
import { popupRouterCacheGuard } from "../platform/popup/view-cache/popup-router-cache.service";
import { CredentialGeneratorHistoryComponent } from "../tools/popup/generator/credential-generator-history.component";

View File

@@ -25,13 +25,13 @@ import {
import { AccountComponent } from "../auth/popup/account-switching/account.component";
import { CurrentAccountComponent } from "../auth/popup/account-switching/current-account.component";
import { ExtensionAnonLayoutWrapperComponent } from "../auth/popup/extension-anon-layout-wrapper/extension-anon-layout-wrapper.component";
import { RemovePasswordComponent } from "../auth/popup/remove-password.component";
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { PopOutComponent } from "../platform/popup/components/pop-out.component";
import { HeaderComponent } from "../platform/popup/header.component";
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";

View File

@@ -1,7 +1,7 @@
{
"name": "@bitwarden/cli",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.3.0",
"version": "2025.4.0",
"keywords": [
"bitwarden",
"password",

View File

@@ -17,7 +17,7 @@ import { MasterKey } from "@bitwarden/common/types/key";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { KeyService } from "@bitwarden/key-management";
import { ConvertToKeyConnectorCommand } from "../../commands/convert-to-key-connector.command";
import { ConvertToKeyConnectorCommand } from "../../key-management/convert-to-key-connector.command";
import { Response } from "../../models/response";
import { MessageResponse } from "../../models/response/message.response";
import { CliUtils } from "../../utils";

View File

@@ -410,26 +410,6 @@ dependencies = [
"serde",
]
[[package]]
name = "bindgen"
version = "0.71.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
dependencies = [
"bitflags",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn",
]
[[package]]
name = "bitflags"
version = "2.8.0"
@@ -573,15 +553,6 @@ dependencies = [
"shlex",
]
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -622,17 +593,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "clap"
version = "4.5.31"
@@ -987,6 +947,7 @@ dependencies = [
"base64",
"desktop_core",
"hex",
"log",
"napi",
"napi-build",
"napi-derive",
@@ -1492,15 +1453,6 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itertools"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.14"
@@ -2302,16 +2254,6 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
@@ -2455,9 +2397,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rsa"
version = "0.9.6"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc"
checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
dependencies = [
"const-oid",
"digest",
@@ -2490,12 +2432,6 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
@@ -3735,7 +3671,6 @@ checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
name = "windows_plugin_authenticator"
version = "0.0.0"
dependencies = [
"bindgen",
"hex",
"windows 0.61.1",
"windows-core 0.61.0",

View File

@@ -38,7 +38,7 @@ oslog = "=0.2.0"
pin-project = "=1.1.8"
pkcs8 = "=0.10.2"
rand = "=0.8.5"
rsa = "=0.9.6"
rsa = "=0.9.8"
russh-cryptovec = "=0.7.3"
scopeguard = "=1.2.0"
security-framework = "=3.1.0"

View File

@@ -18,6 +18,7 @@ base64 = { workspace = true }
hex = { workspace = true }
anyhow = { workspace = true }
desktop_core = { path = "../core" }
log = { workspace = true }
napi = { workspace = true, features = ["async"] }
napi-derive = { workspace = true }
serde = { workspace = true, features = ["derive"] }

View File

@@ -185,3 +185,13 @@ export declare namespace crypto {
export declare namespace passkey_authenticator {
export function register(): void
}
export declare namespace logging {
export const enum LogLevel {
Trace = 0,
Debug = 1,
Info = 2,
Warn = 3,
Error = 4
}
export function initNapiLog(jsLogFn: (err: Error | null, arg0: LogLevel, arg1: string) => any): void
}

View File

@@ -807,3 +807,61 @@ pub mod passkey_authenticator {
})
}
}
#[napi]
pub mod logging {
use log::{Level, Metadata, Record};
use napi::threadsafe_function::{
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
use std::sync::OnceLock;
struct JsLogger(OnceLock<ThreadsafeFunction<(LogLevel, String), CalleeHandled>>);
static JS_LOGGER: JsLogger = JsLogger(OnceLock::new());
#[napi]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl From<Level> for LogLevel {
fn from(level: Level) -> Self {
match level {
Level::Trace => LogLevel::Trace,
Level::Debug => LogLevel::Debug,
Level::Info => LogLevel::Info,
Level::Warn => LogLevel::Warn,
Level::Error => LogLevel::Error,
}
}
}
#[napi]
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) {
let _ = JS_LOGGER.0.set(js_log_fn);
let _ = log::set_logger(&JS_LOGGER);
log::set_max_level(log::LevelFilter::Debug);
}
impl log::Log for JsLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= log::max_level()
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let Some(logger) = self.0.get() else {
return;
};
let msg = (record.level().into(), record.args().to_string());
let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking);
}
fn flush(&self) {}
}
}

View File

@@ -5,9 +5,6 @@ edition = { workspace = true }
license = { workspace = true }
publish = { workspace = true }
[target.'cfg(target_os = "windows")'.build-dependencies]
bindgen = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true, features = ["Win32_Foundation", "Win32_Security", "Win32_System_Com", "Win32_System_LibraryLoader" ] }
windows-core = { workspace = true }

View File

@@ -2,22 +2,6 @@
This is an internal crate that's meant to be a safe abstraction layer over the generated Rust bindings for the Windows WebAuthn Plugin Authenticator API's.
This crate is very much a WIP and is not ready for internal use.
You can find more information about the Windows WebAuthn API's [here](https://github.com/microsoft/webauthn).
## Building
To build this crate, set the following environment variables:
- `LIBCLANG_PATH` -> the path to the `bin` directory of your LLVM install ([more info](https://rust-lang.github.io/rust-bindgen/requirements.html?highlight=libclang_path#installing-clang))
### Bash Example
```
export LIBCLANG_PATH='C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
```
### PowerShell Example
```
$env:LIBCLANG_PATH = 'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\x64\bin'
```

View File

@@ -1,27 +0,0 @@
fn main() {
#[cfg(target_os = "windows")]
windows();
}
#[cfg(target_os = "windows")]
fn windows() {
let out_dir = std::env::var("OUT_DIR").expect("OUT_DIR not set");
let bindings = bindgen::Builder::default()
.header("pluginauthenticator.hpp")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.allowlist_type("DWORD")
.allowlist_type("PBYTE")
.allowlist_type("EXPERIMENTAL.*")
.allowlist_function(".*EXPERIMENTAL.*")
.allowlist_function("WebAuthNGetApiVersionNumber")
.generate()
.expect("Unable to generate bindings.");
bindings
.write_to_file(format!(
"{}\\windows_plugin_authenticator_bindings.rs",
out_dir
))
.expect("Couldn't write bindings.");
}

View File

@@ -1,231 +0,0 @@
/*
Bitwarden's pluginauthenticator.hpp
Source: https://github.com/microsoft/webauthn/blob/master/experimental/pluginauthenticator.h
This is a C++ header file, so the extension has been manually
changed from `.h` to `.hpp`, so bindgen will automatically
generate the correct C++ bindings.
More Info: https://rust-lang.github.io/rust-bindgen/cpp.html
*/
/* this ALWAYS GENERATED file contains the definitions for the interfaces */
/* File created by MIDL compiler version 8.01.0628 */
/* @@MIDL_FILE_HEADING( ) */
/* verify that the <rpcndr.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCNDR_H_VERSION__
#define __REQUIRED_RPCNDR_H_VERSION__ 501
#endif
/* verify that the <rpcsal.h> version is high enough to compile this file*/
#ifndef __REQUIRED_RPCSAL_H_VERSION__
#define __REQUIRED_RPCSAL_H_VERSION__ 100
#endif
#include "rpc.h"
#include "rpcndr.h"
#ifndef __RPCNDR_H_VERSION__
#error this stub requires an updated version of <rpcndr.h>
#endif /* __RPCNDR_H_VERSION__ */
#ifndef COM_NO_WINDOWS_H
#include "windows.h"
#include "ole2.h"
#endif /*COM_NO_WINDOWS_H*/
#ifndef __pluginauthenticator_h__
#define __pluginauthenticator_h__
#if defined(_MSC_VER) && (_MSC_VER >= 1020)
#pragma once
#endif
#ifndef DECLSPEC_XFGVIRT
#if defined(_CONTROL_FLOW_GUARD_XFG)
#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func))
#else
#define DECLSPEC_XFGVIRT(base, func)
#endif
#endif
/* Forward Declarations */
#ifndef __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
#define __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__
typedef interface EXPERIMENTAL_IPluginAuthenticator EXPERIMENTAL_IPluginAuthenticator;
#endif /* __EXPERIMENTAL_IPluginAuthenticator_FWD_DEFINED__ */
/* header files for imported files */
#include "oaidl.h"
#include "webauthn.h"
#ifdef __cplusplus
extern "C"{
#endif
/* interface __MIDL_itf_pluginauthenticator_0000_0000 */
/* [local] */
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST
{
HWND hWnd;
GUID transactionId;
DWORD cbRequestSignature;
/* [size_is] */ byte *pbRequestSignature;
DWORD cbEncodedRequest;
/* [size_is] */ byte *pbEncodedRequest;
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE
{
DWORD cbEncodedResponse;
/* [size_is] */ byte *pbEncodedResponse;
} EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_RESPONSE;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST
{
GUID transactionId;
DWORD cbRequestSignature;
/* [size_is] */ byte *pbRequestSignature;
} EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
typedef struct _EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
typedef const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST *EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_c_ifspec;
extern RPC_IF_HANDLE __MIDL_itf_pluginauthenticator_0000_0000_v0_0_s_ifspec;
#ifndef __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
#define __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__
/* interface EXPERIMENTAL_IPluginAuthenticator */
/* [unique][version][uuid][object] */
EXTERN_C const IID IID_EXPERIMENTAL_IPluginAuthenticator;
#if defined(__cplusplus) && !defined(CINTERFACE)
MIDL_INTERFACE("e6466e9a-b2f3-47c5-b88d-89bc14a8d998")
EXPERIMENTAL_IPluginAuthenticator : public IUnknown
{
public:
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginMakeCredential(
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginGetAssertion(
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response) = 0;
virtual HRESULT STDMETHODCALLTYPE EXPERIMENTAL_PluginCancelOperation(
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request) = 0;
};
#else /* C style interface */
typedef struct EXPERIMENTAL_IPluginAuthenticatorVtbl
{
BEGIN_INTERFACE
DECLSPEC_XFGVIRT(IUnknown, QueryInterface)
HRESULT ( STDMETHODCALLTYPE *QueryInterface )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in REFIID riid,
/* [annotation][iid_is][out] */
_COM_Outptr_ void **ppvObject);
DECLSPEC_XFGVIRT(IUnknown, AddRef)
ULONG ( STDMETHODCALLTYPE *AddRef )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
DECLSPEC_XFGVIRT(IUnknown, Release)
ULONG ( STDMETHODCALLTYPE *Release )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This);
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginMakeCredential)
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginMakeCredential )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginGetAssertion)
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginGetAssertion )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST request,
/* [out] */ __RPC__deref_out_opt EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE *response);
DECLSPEC_XFGVIRT(EXPERIMENTAL_IPluginAuthenticator, EXPERIMENTAL_PluginCancelOperation)
HRESULT ( STDMETHODCALLTYPE *EXPERIMENTAL_PluginCancelOperation )(
__RPC__in EXPERIMENTAL_IPluginAuthenticator * This,
/* [in] */ __RPC__in EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST request);
END_INTERFACE
} EXPERIMENTAL_IPluginAuthenticatorVtbl;
interface EXPERIMENTAL_IPluginAuthenticator
{
CONST_VTBL struct EXPERIMENTAL_IPluginAuthenticatorVtbl *lpVtbl;
};
#ifdef COBJMACROS
#define EXPERIMENTAL_IPluginAuthenticator_QueryInterface(This,riid,ppvObject) \
( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) )
#define EXPERIMENTAL_IPluginAuthenticator_AddRef(This) \
( (This)->lpVtbl -> AddRef(This) )
#define EXPERIMENTAL_IPluginAuthenticator_Release(This) \
( (This)->lpVtbl -> Release(This) )
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginMakeCredential(This,request,response) \
( (This)->lpVtbl -> EXPERIMENTAL_PluginMakeCredential(This,request,response) )
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginGetAssertion(This,request,response) \
( (This)->lpVtbl -> EXPERIMENTAL_PluginGetAssertion(This,request,response) )
#define EXPERIMENTAL_IPluginAuthenticator_EXPERIMENTAL_PluginCancelOperation(This,request) \
( (This)->lpVtbl -> EXPERIMENTAL_PluginCancelOperation(This,request) )
#endif /* COBJMACROS */
#endif /* C style interface */
#endif /* __EXPERIMENTAL_IPluginAuthenticator_INTERFACE_DEFINED__ */
/* Additional Prototypes for ALL interfaces */
unsigned long __RPC_USER HWND_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
void __RPC_USER HWND_UserFree( __RPC__in unsigned long *, __RPC__in HWND * );
unsigned long __RPC_USER HWND_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in HWND * );
unsigned char * __RPC_USER HWND_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out HWND * );
void __RPC_USER HWND_UserFree64( __RPC__in unsigned long *, __RPC__in HWND * );
/* end of Additional Prototypes */
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -2,15 +2,6 @@
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
mod pa;
use pa::{
DWORD, EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST,
EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST,
EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE,
EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE,
EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE, PBYTE,
};
use std::ffi::c_uchar;
use std::ptr;
use windows::Win32::Foundation::*;
@@ -23,11 +14,53 @@ const AUTHENTICATOR_NAME: &str = "Bitwarden Desktop Authenticator";
const CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
const RPID: &str = "bitwarden.com";
/// Returns the current Windows WebAuthN version.
pub fn get_version_number() -> u32 {
unsafe { pa::WebAuthNGetApiVersionNumber() }
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST {
pub transactionId: GUID,
pub cbRequestSignature: Dword,
pub pbRequestSignature: *mut byte,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST {
pub hWnd: HWND,
pub transactionId: GUID,
pub cbRequestSignature: Dword,
pub pbRequestSignature: *mut byte,
pub cbEncodedRequest: Dword,
pub pbEncodedRequest: *mut byte,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE {
pub cbOpSignPubKey: Dword,
pub pbOpSignPubKey: PByte,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE {
pub cbEncodedResponse: Dword,
pub pbEncodedResponse: *mut byte,
}
type Dword = u32;
type Byte = u8;
type byte = u8;
pub type PByte = *mut Byte;
type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST =
*const EXPERIMENTAL_WEBAUTHN_PLUGIN_CANCEL_OPERATION_REQUEST;
pub type EXPERIMENTAL_PCWEBAUTHN_PLUGIN_OPERATION_REQUEST =
*const EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_REQUEST;
pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_OPERATION_RESPONSE =
*mut EXPERIMENTAL_WEBAUTHN_PLUGIN_OPERATION_RESPONSE;
pub type EXPERIMENTAL_PWEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE =
*mut EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE;
/// Handles initialization and registration for the Bitwarden desktop app as a
/// plugin authenticator with Windows.
/// For now, also adds the authenticator
@@ -123,9 +156,9 @@ fn add_authenticator() -> std::result::Result<(), String> {
pbAuthenticatorInfo: authenticator_info_bytes.as_mut_ptr(),
};
let plugin_signing_public_key_byte_count: DWORD = 0;
let plugin_signing_public_key_byte_count: Dword = 0;
let mut plugin_signing_public_key: c_uchar = 0;
let plugin_signing_public_key_ptr: PBYTE = &mut plugin_signing_public_key;
let plugin_signing_public_key_ptr: PByte = &mut plugin_signing_public_key;
let mut add_response = EXPERIMENTAL_WEBAUTHN_PLUGIN_ADD_AUTHENTICATOR_RESPONSE {
cbOpSignPubKey: plugin_signing_public_key_byte_count,

View File

@@ -1,15 +0,0 @@
/*
The 'pa' (plugin authenticator) module will contain the generated
bindgen code.
The attributes below will suppress warnings from the generated code.
*/
#![cfg(target_os = "windows")]
#![allow(clippy::all)]
#![allow(warnings)]
include!(concat!(
env!("OUT_DIR"),
"/windows_plugin_authenticator_bindings.rs"
));

View File

@@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.4.1",
"version": "2025.4.2",
"keywords": [
"bitwarden",
"password",

View File

@@ -50,9 +50,9 @@ import {
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
import { maxAccountsGuardFn } from "../auth/guards/max-accounts.guard";
import { RemovePasswordComponent } from "../auth/remove-password.component";
import { SetPasswordComponent } from "../auth/set-password.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { VaultComponent } from "../vault/app/vault/vault.component";
import { Fido2PlaceholderComponent } from "./components/fido2placeholder.component";

View File

@@ -13,11 +13,11 @@ import { DecryptionFailureDialogComponent } from "@bitwarden/vault";
import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component";
import { DeleteAccountComponent } from "../auth/delete-account.component";
import { LoginModule } from "../auth/login/login.module";
import { RemovePasswordComponent } from "../auth/remove-password.component";
import { SetPasswordComponent } from "../auth/set-password.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
import { SshAgentService } from "../autofill/services/ssh-agent.service";
import { PremiumComponent } from "../billing/app/accounts/premium.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { AddEditCustomFieldsComponent } from "../vault/app/vault/add-edit-custom-fields.component";
import { AddEditComponent } from "../vault/app/vault/add-edit.component";
import { AttachmentsComponent } from "../vault/app/vault/attachments.component";

View File

@@ -300,7 +300,7 @@ describe("MainBiometricsService", function () {
expect(userKey).not.toBeNull();
expect(userKey!.keyB64).toBe(biometricKey);
expect(userKey!.encType).toBe(EncryptionType.AesCbc256_HmacSha256_B64);
expect(userKey!.inner().type).toBe(EncryptionType.AesCbc256_HmacSha256_B64);
expect(osBiometricsService.getBiometricKey).toHaveBeenCalledWith(
"Bitwarden_biometric",
`${userId}_user_biometric`,

View File

@@ -1,5 +1,7 @@
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { biometrics, passwords } from "@bitwarden/desktop-napi";
@@ -218,7 +220,13 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
symmetricKey: SymmetricCryptoKey,
clientKeyPartB64: string | undefined,
): biometrics.KeyMaterial {
const key = symmetricKey?.macKeyB64 ?? symmetricKey?.keyB64;
let key = null;
const innerKey = symmetricKey.inner();
if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
key = Utils.fromBufferToB64(innerKey.authenticationKey);
} else {
key = Utils.fromBufferToB64(innerKey.encryptionKey);
}
const result = {
osKeyPartB64: key,

View File

@@ -1,6 +1,6 @@
import { Component } from "@angular/core";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
@Component({
selector: "app-remove-password",

View File

@@ -292,11 +292,7 @@ export class WindowMain {
this.win.maximize();
}
// Show it later since it might need to be maximized.
// use once event to avoid flash on unstyled content.
this.win.once("ready-to-show", () => {
this.win.show();
});
this.win.show();
if (template === "full-app") {
// and load the index.html of the app.

View File

@@ -1,12 +1,12 @@
{
"name": "@bitwarden/desktop",
"version": "2025.4.1",
"version": "2025.4.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@bitwarden/desktop",
"version": "2025.4.1",
"version": "2025.4.2",
"license": "GPL-3.0",
"dependencies": {
"@bitwarden/desktop-napi": "file:../desktop_native/napi"

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/desktop",
"productName": "Bitwarden",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.4.1",
"version": "2025.4.2",
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
"homepage": "https://bitwarden.com",
"license": "GPL-3.0",

View File

@@ -7,6 +7,7 @@ import log from "electron-log/main";
import { LogLevelType } from "@bitwarden/common/platform/enums/log-level-type.enum";
import { ConsoleLogService as BaseLogService } from "@bitwarden/common/platform/services/console-log.service";
import { logging } from "@bitwarden/desktop-napi";
import { isDev } from "../../utils";
@@ -30,6 +31,29 @@ export class ElectronLogMainService extends BaseLogService {
ipcMain.handle("ipc.log", (_event, { level, message, optionalParams }) => {
this.write(level, message, ...optionalParams);
});
logging.initNapiLog((error, level, message) => this.writeNapiLog(level, message));
}
private writeNapiLog(level: logging.LogLevel, message: string) {
let levelType: LogLevelType;
switch (level) {
case logging.LogLevel.Debug:
levelType = LogLevelType.Debug;
break;
case logging.LogLevel.Warn:
levelType = LogLevelType.Warning;
break;
case logging.LogLevel.Error:
levelType = LogLevelType.Error;
break;
default:
levelType = LogLevelType.Info;
break;
}
this.write(levelType, "[NAPI] " + message);
}
write(level: LogLevelType, message?: any, ...optionalParams: any[]) {

View File

@@ -5,6 +5,14 @@ jest.mock("electron", () => ({
ipcMain: { handle: jest.fn(), on: jest.fn() },
}));
jest.mock("@bitwarden/desktop-napi", () => {
return {
logging: {
initNapiLog: jest.fn(),
},
};
});
describe("ElectronLogMainService", () => {
it("sets dev based on electron method", () => {
process.env.ELECTRON_IS_DEV = "1";

View File

@@ -1,6 +1,6 @@
{
"name": "@bitwarden/web-vault",
"version": "2025.4.0",
"version": "2025.4.1",
"scripts": {
"build:oss": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack",
"build:bit": "cross-env NODE_OPTIONS=\"--max-old-space-size=8192\" webpack -c ../../bitwarden_license/bit-web/webpack.config.js",

View File

@@ -100,20 +100,44 @@ describe("Organization Permissions Guard", () => {
it("permits navigation if the user has permissions", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockImplementation((_org) => true);
permissionsCallback.mockReturnValue(true);
const actual = await TestBed.runInInjectionContext(
async () => await organizationPermissionsGuard(permissionsCallback)(route, state),
);
expect(permissionsCallback).toHaveBeenCalledWith(orgFactory({ id: targetOrgId }));
expect(permissionsCallback).toHaveBeenCalledTimes(1);
expect(actual).toBe(true);
});
it("handles a Promise returned from the callback", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockReturnValue(Promise.resolve(true));
const actual = await TestBed.runInInjectionContext(() =>
organizationPermissionsGuard(permissionsCallback)(route, state),
);
expect(permissionsCallback).toHaveBeenCalledTimes(1);
expect(actual).toBe(true);
});
it("handles an Observable returned from the callback", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockReturnValue(of(true));
const actual = await TestBed.runInInjectionContext(() =>
organizationPermissionsGuard(permissionsCallback)(route, state),
);
expect(permissionsCallback).toHaveBeenCalledTimes(1);
expect(actual).toBe(true);
});
describe("if the user does not have permissions", () => {
it("and there is no Item ID, block navigation", async () => {
const permissionsCallback = jest.fn();
permissionsCallback.mockImplementation((_org) => false);
permissionsCallback.mockReturnValue(false);
state = mock<RouterStateSnapshot>({
root: mock<ActivatedRouteSnapshot>({

View File

@@ -1,13 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { inject } from "@angular/core";
import { EnvironmentInjector, inject, runInInjectionContext } from "@angular/core";
import {
ActivatedRouteSnapshot,
CanActivateFn,
Router,
RouterStateSnapshot,
} from "@angular/router";
import { firstValueFrom, switchMap } from "rxjs";
import { firstValueFrom, isObservable, Observable, switchMap } from "rxjs";
import {
canAccessOrgAdmin,
@@ -42,7 +42,9 @@ import { ToastService } from "@bitwarden/components";
* proceeds as expected.
*/
export function organizationPermissionsGuard(
permissionsCallback?: (organization: Organization) => boolean,
permissionsCallback?: (
organization: Organization,
) => boolean | Promise<boolean> | Observable<boolean>,
): CanActivateFn {
return async (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => {
const router = inject(Router);
@@ -51,6 +53,7 @@ export function organizationPermissionsGuard(
const i18nService = inject(I18nService);
const syncService = inject(SyncService);
const accountService = inject(AccountService);
const environmentInjector = inject(EnvironmentInjector);
// TODO: We need to fix issue once and for all.
if ((await syncService.getLastSync()) == null) {
@@ -78,7 +81,22 @@ export function organizationPermissionsGuard(
return router.createUrlTree(["/"]);
}
const hasPermissions = permissionsCallback == null || permissionsCallback(org);
if (permissionsCallback == null) {
// No additional permission checks required, allow navigation
return true;
}
const callbackResult = runInInjectionContext(environmentInjector, () =>
permissionsCallback(org),
);
const hasPermissions = isObservable(callbackResult)
? await firstValueFrom(callbackResult) // handles observables
: await Promise.resolve(callbackResult); // handles promises and boolean values
if (hasPermissions !== true && hasPermissions !== false) {
throw new Error("Permission callback did not resolve to a boolean.");
}
if (!hasPermissions) {
// Handle linkable ciphers for organizations the user only has view access to

View File

@@ -97,7 +97,7 @@
<bit-nav-item
[text]="'policies' | i18n"
route="settings/policies"
*ngIf="organization.canManagePolicies"
*ngIf="canShowPoliciesTab$ | async"
></bit-nav-item>
<bit-nav-item
[text]="'twoStepLogin' | i18n"

View File

@@ -22,6 +22,7 @@ import { PolicyType, ProviderStatusType } from "@bitwarden/common/admin-console/
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -68,6 +69,7 @@ export class OrganizationLayoutComponent implements OnInit {
showAccountDeprovisioningBanner$: Observable<boolean>;
protected isBreadcrumbEventLogsEnabled$: Observable<boolean>;
protected showSponsoredFamiliesDropdown$: Observable<boolean>;
protected canShowPoliciesTab$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
@@ -79,6 +81,7 @@ export class OrganizationLayoutComponent implements OnInit {
protected bannerService: AccountDeprovisioningBannerService,
private accountService: AccountService,
private freeFamiliesPolicyService: FreeFamiliesPolicyService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
) {}
async ngOnInit() {
@@ -148,6 +151,18 @@ export class OrganizationLayoutComponent implements OnInit {
))
? "claimedDomains"
: "domainVerification";
this.canShowPoliciesTab$ = this.organization$.pipe(
switchMap((organization) =>
this.organizationBillingService
.isBreadcrumbingPoliciesEnabled$(organization)
.pipe(
map(
(isBreadcrumbingEnabled) => isBreadcrumbingEnabled || organization.canManagePolicies,
),
),
),
);
}
canShowVaultTab(organization: Organization): boolean {

View File

@@ -1,4 +1,17 @@
<app-header></app-header>
<app-header>
@let organization = organization$ | async;
<button
bitBadge
class="!tw-align-middle"
(click)="changePlan(organization)"
*ngIf="isBreadcrumbingEnabled$ | async"
slot="title-suffix"
type="button"
variant="primary"
>
{{ "upgrade" | i18n }}
</button>
</app-header>
<bit-container>
<ng-container *ngIf="loading">

View File

@@ -2,8 +2,8 @@
// @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, lastValueFrom } from "rxjs";
import { first, map } from "rxjs/operators";
import { firstValueFrom, lastValueFrom, map, Observable, switchMap } from "rxjs";
import { first } from "rxjs/operators";
import {
getOrganizationById,
@@ -14,10 +14,17 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { DialogService } from "@bitwarden/components";
import {
ChangePlanDialogResultType,
openChangePlanDialog,
} from "@bitwarden/web-vault/app/billing/organizations/change-plan-dialog.component";
import { All } from "@bitwarden/web-vault/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model";
import { PolicyListService } from "../../core/policy-list.service";
import { BasePolicy } from "../policies";
import { CollectionDialogTabType } from "../shared/components/collection-dialog";
import { PolicyEditComponent, PolicyEditDialogResult } from "./policy-edit.component";
@@ -32,17 +39,19 @@ export class PoliciesComponent implements OnInit {
loading = true;
organizationId: string;
policies: BasePolicy[];
organization: Organization;
protected organization$: Observable<Organization>;
private orgPolicies: PolicyResponse[];
protected policiesEnabledMap: Map<PolicyType, boolean> = new Map<PolicyType, boolean>();
protected isBreadcrumbingEnabled$: Observable<boolean>;
constructor(
private route: ActivatedRoute,
private organizationService: OrganizationService,
private accountService: AccountService,
private organizationService: OrganizationService,
private policyApiService: PolicyApiServiceAbstraction,
private policyListService: PolicyListService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
private dialogService: DialogService,
) {}
@@ -53,11 +62,9 @@ export class PoliciesComponent implements OnInit {
const userId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.organization = await firstValueFrom(
this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(this.organizationId)),
);
this.organization$ = this.organizationService
.organizations$(userId)
.pipe(getOrganizationById(this.organizationId));
this.policies = this.policyListService.getPolicies();
await this.load();
@@ -91,7 +98,11 @@ export class PoliciesComponent implements OnInit {
this.orgPolicies.forEach((op) => {
this.policiesEnabledMap.set(op.type, op.enabled);
});
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
switchMap((organization) =>
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
),
);
this.loading = false;
}
@@ -104,8 +115,34 @@ export class PoliciesComponent implements OnInit {
});
const result = await lastValueFrom(dialogRef.closed);
if (result === PolicyEditDialogResult.Saved) {
await this.load();
switch (result) {
case PolicyEditDialogResult.Saved:
await this.load();
break;
case PolicyEditDialogResult.UpgradePlan:
await this.changePlan(await firstValueFrom(this.organization$));
break;
}
}
protected readonly CollectionDialogTabType = CollectionDialogTabType;
protected readonly All = All;
protected async changePlan(organization: Organization) {
const reference = openChangePlanDialog(this.dialogService, {
data: {
organizationId: organization.id,
subscription: null,
productTierType: organization.productTierType,
},
});
const result = await lastValueFrom(reference.closed);
if (result === ChangePlanDialogResultType.Closed) {
return;
}
await this.load();
}
}

View File

@@ -1,5 +1,17 @@
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog [loading]="loading" [title]="'editPolicy' | i18n" [subtitle]="policy.name | i18n">
<ng-container bitDialogTitle>
<button
bitBadge
class="!tw-align-middle"
(click)="upgradePlan()"
*ngIf="isBreadcrumbingEnabled$ | async"
type="button"
variant="primary"
>
{{ "planNameEnterprise" | i18n }}
</button>
</ng-container>
<ng-container bitDialogContent>
<div *ngIf="loading">
<i
@@ -16,6 +28,7 @@
</ng-container>
<ng-container bitDialogFooter>
<button
*ngIf="!(isBreadcrumbingEnabled$ | async); else breadcrumbing"
bitButton
buttonType="primary"
[disabled]="saveDisabled$ | async"
@@ -24,6 +37,11 @@
>
{{ "save" | i18n }}
</button>
<ng-template #breadcrumbing>
<button bitButton buttonType="primary" bitFormButton type="button" (click)="upgradePlan()">
{{ "upgrade" | i18n }}
</button>
</ng-template>
<button bitButton buttonType="secondary" bitDialogClose type="button">
{{ "cancel" | i18n }}
</button>

View File

@@ -9,12 +9,20 @@ import {
ViewContainerRef,
} from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { Observable, map } from "rxjs";
import { map, Observable, switchMap } from "rxjs";
import {
getOrganizationById,
OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { PolicyRequest } from "@bitwarden/common/admin-console/models/request/policy.request";
import { PolicyResponse } from "@bitwarden/common/admin-console/models/response/policy.response";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import {
DIALOG_DATA,
@@ -35,6 +43,7 @@ export type PolicyEditDialogData = {
export enum PolicyEditDialogResult {
Saved = "saved",
UpgradePlan = "upgrade-plan",
}
@Component({
selector: "app-policy-edit",
@@ -48,22 +57,28 @@ export class PolicyEditComponent implements AfterViewInit {
loading = true;
enabled = false;
saveDisabled$: Observable<boolean>;
defaultTypes: any[];
policyComponent: BasePolicyComponent;
private policyResponse: PolicyResponse;
formGroup = this.formBuilder.group({
enabled: [this.enabled],
});
protected organization$: Observable<Organization>;
protected isBreadcrumbingEnabled$: Observable<boolean>;
constructor(
@Inject(DIALOG_DATA) protected data: PolicyEditDialogData,
private accountService: AccountService,
private policyApiService: PolicyApiServiceAbstraction,
private organizationService: OrganizationService,
private i18nService: I18nService,
private cdr: ChangeDetectorRef,
private formBuilder: FormBuilder,
private dialogRef: DialogRef<PolicyEditDialogResult>,
private toastService: ToastService,
private organizationBillingService: OrganizationBillingServiceAbstraction,
) {}
get policy(): BasePolicy {
return this.data.policy;
}
@@ -97,6 +112,16 @@ export class PolicyEditComponent implements AfterViewInit {
throw e;
}
}
this.organization$ = this.accountService.activeAccount$.pipe(
getUserId,
switchMap((userId) => this.organizationService.organizations$(userId)),
getOrganizationById(this.data.organizationId),
);
this.isBreadcrumbingEnabled$ = this.organization$.pipe(
switchMap((organization) =>
this.organizationBillingService.isBreadcrumbingPoliciesEnabled$(organization),
),
);
}
submit = async () => {
@@ -119,4 +144,8 @@ export class PolicyEditComponent implements AfterViewInit {
static open = (dialogService: DialogService, config: DialogConfig<PolicyEditDialogData>) => {
return dialogService.open<PolicyEditDialogResult>(PolicyEditComponent, config);
};
protected upgradePlan(): void {
this.dialogRef.close(PolicyEditDialogResult.UpgradePlan);
}
}

View File

@@ -1,8 +1,10 @@
import { NgModule } from "@angular/core";
import { NgModule, inject } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { map } from "rxjs";
import { canAccessSettingsTab } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { organizationPermissionsGuard } from "../../organizations/guards/org-permissions.guard";
import { organizationRedirectGuard } from "../../organizations/guards/org-redirect.guard";
@@ -41,7 +43,14 @@ const routes: Routes = [
{
path: "policies",
component: PoliciesComponent,
canActivate: [organizationPermissionsGuard((org) => org.canManagePolicies)],
canActivate: [
organizationPermissionsGuard((o: Organization) => {
const organizationBillingService = inject(OrganizationBillingServiceAbstraction);
return organizationBillingService
.isBreadcrumbingPoliciesEnabled$(o)
.pipe(map((isBreadcrumbingEnabled) => o.canManagePolicies || isBreadcrumbingEnabled));
}),
],
data: {
titleId: "policies",
},

View File

@@ -1,6 +1,7 @@
<div class="tw-mt-10 tw-flex tw-justify-center" *ngIf="loading">
<div>
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo"></bit-icon>
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n">
</bit-icon>
<div class="tw-flex tw-justify-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"

View File

@@ -24,7 +24,6 @@ import { OrgKey } from "@bitwarden/common/types/key";
import { DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component";
import { I18nService } from "../../core/i18n.service";
import {
@@ -200,11 +199,6 @@ describe("AcceptOrganizationInviteService", () => {
encryptedString: "encryptedString",
} as EncString);
jest.mock("../../admin-console/organizations/manage/organization-trust.component");
OrganizationTrustComponent.open = jest.fn().mockReturnValue({
closed: new BehaviorSubject(true),
});
await globalState.update(() => invite);
policyService.getResetPasswordPolicyOptions.mockReturnValue([
@@ -217,7 +211,6 @@ describe("AcceptOrganizationInviteService", () => {
const result = await sut.validateAndAcceptInvite(invite);
expect(result).toBe(true);
expect(OrganizationTrustComponent.open).toHaveBeenCalled();
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
{ key: "userKey" },
Utils.fromB64ToArray("publicKey"),

View File

@@ -31,8 +31,6 @@ import { OrgKey } from "@bitwarden/common/types/key";
import { DialogService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
import { OrganizationTrustComponent } from "../../admin-console/organizations/manage/organization-trust.component";
import { OrganizationInvite } from "./organization-invite";
// We're storing the organization invite for 2 reasons:
@@ -189,15 +187,6 @@ export class AcceptOrganizationInviteService {
}
const publicKey = Utils.fromB64ToArray(response.publicKey);
const dialogRef = OrganizationTrustComponent.open(this.dialogService, {
name: invite.organizationName,
orgId: invite.organizationId,
publicKey,
});
const result = await firstValueFrom(dialogRef.closed);
if (result !== true) {
throw new Error("Organization not trusted, aborting user key rotation");
}
const activeUserId = (await firstValueFrom(this.accountService.activeAccount$)).id;
const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId));

View File

@@ -76,7 +76,7 @@ export class AddCreditDialogComponent implements OnInit {
async ngOnInit() {
if (this.organizationId != null) {
if (this.creditAmount == null) {
this.creditAmount = "20.00";
this.creditAmount = "0.00";
}
this.ppButtonCustomField = "organization_id:" + this.organizationId;
const userId = await firstValueFrom(
@@ -93,7 +93,7 @@ export class AddCreditDialogComponent implements OnInit {
}
} else {
if (this.creditAmount == null) {
this.creditAmount = "10.00";
this.creditAmount = "0.00";
}
const [userId, email] = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => [a?.id, a?.email])),

View File

@@ -1,6 +1,6 @@
import { Component } from "@angular/core";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/key-management-ui";
@Component({
selector: "app-remove-password",

View File

@@ -12,7 +12,7 @@
<h1
bitTypography="h1"
noMargin
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex"
class="tw-m-0 tw-mr-2 tw-leading-10 tw-flex tw-gap-1"
[title]="title || (routeData.titleId | i18n)"
>
<div class="tw-truncate">

View File

@@ -58,7 +58,6 @@ import { LoginViaWebAuthnComponent } from "./auth/login/login-via-webauthn/login
import { AcceptOrganizationComponent } from "./auth/organization-invite/accept-organization.component";
import { RecoverDeleteComponent } from "./auth/recover-delete.component";
import { RecoverTwoFactorComponent } from "./auth/recover-two-factor.component";
import { RemovePasswordComponent } from "./auth/remove-password.component";
import { SetPasswordComponent } from "./auth/set-password.component";
import { AccountComponent } from "./auth/settings/account/account.component";
import { EmergencyAccessComponent } from "./auth/settings/emergency-access/emergency-access.component";
@@ -73,6 +72,7 @@ import { CompleteTrialInitiationComponent } from "./billing/trial-initiation/com
import { freeTrialTextResolver } from "./billing/trial-initiation/complete-trial-initiation/resolver/free-trial-text.resolver";
import { EnvironmentSelectorComponent } from "./components/environment-selector/environment-selector.component";
import { RouteDataProperties } from "./core";
import { RemovePasswordComponent } from "./key-management/key-connector/remove-password.component";
import { FrontendLayoutComponent } from "./layouts/frontend-layout.component";
import { UserLayoutComponent } from "./layouts/user-layout.component";
import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-landing/request-sm-access.component";

View File

@@ -15,7 +15,6 @@ import { VerifyRecoverDeleteOrgComponent } from "../admin-console/organizations/
import { AcceptFamilySponsorshipComponent } from "../admin-console/organizations/sponsorships/accept-family-sponsorship.component";
import { RecoverDeleteComponent } from "../auth/recover-delete.component";
import { RecoverTwoFactorComponent } from "../auth/recover-two-factor.component";
import { RemovePasswordComponent } from "../auth/remove-password.component";
import { SetPasswordComponent } from "../auth/set-password.component";
import { AccountComponent } from "../auth/settings/account/account.component";
import { ChangeAvatarDialogComponent } from "../auth/settings/account/change-avatar-dialog.component";
@@ -42,6 +41,7 @@ import { SponsoredFamiliesComponent } from "../billing/settings/sponsored-famili
import { SponsoringOrgRowComponent } from "../billing/settings/sponsoring-org-row.component";
import { DynamicAvatarComponent } from "../components/dynamic-avatar.component";
import { SelectableAvatarComponent } from "../components/selectable-avatar.component";
import { RemovePasswordComponent } from "../key-management/key-connector/remove-password.component";
import { FrontendLayoutComponent } from "../layouts/frontend-layout.component";
import { HeaderModule } from "../layouts/header/header.module";
import { ProductSwitcherModule } from "../layouts/product-switcher/product-switcher.module";

View File

@@ -2,6 +2,9 @@
"allApplications": {
"message": "All applications"
},
"appLogoLabel": {
"message": "Bitwarden logo"
},
"criticalApplications": {
"message": "Critical applications"
},

View File

@@ -1,6 +1,10 @@
<div class="tw-mt-5 tw-flex tw-justify-center" *ngIf="loading">
<div>
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo"></bit-icon>
<bit-icon
class="tw-w-72 tw-block tw-mb-4"
[icon]="logo"
[ariaLabel]="'appLogoLabel' | i18n"
></bit-icon>
<p class="tw-text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"

View File

@@ -1,6 +1,10 @@
<div class="tw-mt-12 tw-flex tw-justify-center" *ngIf="loading">
<div>
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="logo"></bit-icon>
<bit-icon
class="tw-w-72 tw-block tw-mb-4"
[icon]="logo"
[ariaLabel]="'appLogoLabel' | i18n"
></bit-icon>
<p class="tw-text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"

View File

@@ -1,6 +1,10 @@
<div class="tw-mt-12 tw-flex tw-justify-center" *ngIf="loading">
<div>
<bit-icon class="tw-w-72 tw-block tw-mb-4" [icon]="bitwardenLogo"></bit-icon>
<bit-icon
class="tw-w-72 tw-block tw-mb-4"
[icon]="bitwardenLogo"
[ariaLabel]="'appLogoLabel' | i18n"
></bit-icon>
<p class="tw-text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x tw-text-muted"

View File

@@ -229,3 +229,41 @@ export const memberAccessReportsMock: MemberAccessResponse[] = [
],
} as MemberAccessResponse,
];
export const memberAccessWithoutAccessDetailsReportsMock: MemberAccessResponse[] = [
{
userName: "Alice Smith",
email: "asmith@email.com",
twoFactorEnabled: true,
accountRecoveryEnabled: true,
groupsCount: 2,
collectionsCount: 4,
totalItemCount: 20,
userGuid: "1234",
usesKeyConnector: false,
accessDetails: [
{
groupId: "",
collectionId: "c1",
collectionName: new EncString("Collection 1"),
groupName: "Alice Group 1",
itemCount: 10,
readOnly: false,
hidePasswords: false,
manage: false,
} as MemberAccessDetails,
],
} as MemberAccessResponse,
{
userName: "Robert Brown",
email: "rbrown@email.com",
twoFactorEnabled: false,
accountRecoveryEnabled: false,
groupsCount: 2,
collectionsCount: 4,
totalItemCount: 20,
userGuid: "5678",
usesKeyConnector: false,
accessDetails: [] as MemberAccessDetails[],
} as MemberAccessResponse,
];

View File

@@ -4,7 +4,10 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { OrganizationId } from "@bitwarden/common/types/guid";
import { MemberAccessReportApiService } from "./member-access-report-api.service";
import { memberAccessReportsMock } from "./member-access-report.mock";
import {
memberAccessReportsMock,
memberAccessWithoutAccessDetailsReportsMock,
} from "./member-access-report.mock";
import { MemberAccessReportService } from "./member-access-report.service";
describe("ImportService", () => {
const mockOrganizationId = "mockOrgId" as OrganizationId;
@@ -112,5 +115,34 @@ describe("ImportService", () => {
]),
);
});
it("should generate user report export items and include users with no access", async () => {
reportApiService.getMemberAccessData.mockImplementation(() =>
Promise.resolve(memberAccessWithoutAccessDetailsReportsMock),
);
const result =
await memberAccessReportService.generateUserReportExportItems(mockOrganizationId);
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
email: "asmith@email.com",
name: "Alice Smith",
twoStepLogin: "memberAccessReportTwoFactorEnabledTrue",
accountRecovery: "memberAccessReportAuthenticationEnabledTrue",
group: "Alice Group 1",
totalItems: "10",
}),
expect.objectContaining({
email: "rbrown@email.com",
name: "Robert Brown",
twoStepLogin: "memberAccessReportTwoFactorEnabledFalse",
accountRecovery: "memberAccessReportAuthenticationEnabledFalse",
group: "memberAccessReportNoGroup",
totalItems: "0",
}),
]),
);
});
});
});

View File

@@ -65,6 +65,26 @@ export class MemberAccessReportService {
}
const exportItems = memberAccessReports.flatMap((report) => {
// to include users without access details
// which means a user has no groups, collections or items
if (report.accessDetails.length === 0) {
return [
{
email: report.email,
name: report.userName,
twoStepLogin: report.twoFactorEnabled
? this.i18nService.t("memberAccessReportTwoFactorEnabledTrue")
: this.i18nService.t("memberAccessReportTwoFactorEnabledFalse"),
accountRecovery: report.accountRecoveryEnabled
? this.i18nService.t("memberAccessReportAuthenticationEnabledTrue")
: this.i18nService.t("memberAccessReportAuthenticationEnabledFalse"),
group: this.i18nService.t("memberAccessReportNoGroup"),
collection: this.i18nService.t("memberAccessReportNoCollection"),
collectionPermission: this.i18nService.t("memberAccessReportNoCollectionPermission"),
totalItems: "0",
},
];
}
const userDetails = report.accessDetails.map((detail) => {
const collectionName = collectionNameMap.get(detail.collectionName.encryptedString);
return {

View File

@@ -1255,6 +1255,7 @@ const safeProviders: SafeProvider[] = [
I18nServiceAbstraction,
OrganizationApiServiceAbstraction,
SyncService,
ConfigService,
],
}),
safeProvider({

View File

@@ -10,7 +10,7 @@
[routerLink]="['/']"
class="tw-w-[128px] tw-block tw-mb-12 [&>*]:tw-align-top"
>
<bit-icon [icon]="logo"></bit-icon>
<bit-icon [icon]="logo" [ariaLabel]="'appLogoLabel' | i18n"></bit-icon>
</a>
<div

View File

@@ -106,7 +106,9 @@ describe("AuthRequestService", () => {
});
it("should use the master key and hash if they exist", async () => {
masterPasswordService.masterKeySubject.next({ encKey: new Uint8Array(64) } as MasterKey);
masterPasswordService.masterKeySubject.next(
new SymmetricCryptoKey(new Uint8Array(32)) as MasterKey,
);
masterPasswordService.masterKeyHashSubject.next("MASTER_KEY_HASH");
await sut.approveOrDenyAuthRequest(
@@ -115,7 +117,7 @@ describe("AuthRequestService", () => {
);
expect(encryptService.encapsulateKeyUnsigned).toHaveBeenCalledWith(
{ encKey: new Uint8Array(64) },
new SymmetricCryptoKey(new Uint8Array(32)),
expect.anything(),
);
});

View File

@@ -14,6 +14,7 @@ import { AuthRequestPushNotification } from "@bitwarden/common/models/response/n
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
AUTH_REQUEST_DISK_LOCAL,
StateProvider,
@@ -120,7 +121,10 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
keyToEncrypt = await this.keyService.getUserKey();
}
const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(keyToEncrypt, pubKey);
const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(
keyToEncrypt as SymmetricCryptoKey,
pubKey,
);
const response = new PasswordlessAuthRequest(
encryptedKey.encryptedString,

View File

@@ -15,6 +15,9 @@ export class DeviceResponse extends BaseResponse {
creationDate: string;
revisionDate: string;
isTrusted: boolean;
encryptedUserKey: string | null;
encryptedPublicKey: string | null;
devicePendingAuthRequest: DevicePendingAuthRequest | null;
constructor(response: any) {
@@ -27,6 +30,8 @@ export class DeviceResponse extends BaseResponse {
this.creationDate = this.getResponseProperty("CreationDate");
this.revisionDate = this.getResponseProperty("RevisionDate");
this.isTrusted = this.getResponseProperty("IsTrusted");
this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey");
this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey");
this.devicePendingAuthRequest = this.getResponseProperty("DevicePendingAuthRequest");
}
}

View File

@@ -1,5 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { OrganizationResponse } from "../../admin-console/models/response/organization.response";
import { InitiationPath } from "../../models/request/reference-event.request";
@@ -59,4 +62,10 @@ export abstract class OrganizationBillingServiceAbstraction {
organizationId: string,
subscription: SubscriptionInformation,
) => Promise<void>;
/**
* Determines if breadcrumbing policies is enabled for the organizations meeting certain criteria.
* @param organization
*/
abstract isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable<boolean>;
}

View File

@@ -0,0 +1,149 @@
import { mock } from "jest-mock-extended";
import { firstValueFrom, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { OrganizationApiServiceAbstraction as OrganizationApiService } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { ProductTierType } from "@bitwarden/common/billing/enums";
import { OrganizationBillingService } from "@bitwarden/common/billing/services/organization-billing.service";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SyncService } from "@bitwarden/common/platform/sync";
import { KeyService } from "@bitwarden/key-management";
describe("BillingAccountProfileStateService", () => {
let apiService: jest.Mocked<ApiService>;
let billingApiService: jest.Mocked<BillingApiServiceAbstraction>;
let keyService: jest.Mocked<KeyService>;
let encryptService: jest.Mocked<EncryptService>;
let i18nService: jest.Mocked<I18nService>;
let organizationApiService: jest.Mocked<OrganizationApiService>;
let syncService: jest.Mocked<SyncService>;
let configService: jest.Mocked<ConfigService>;
let sut: OrganizationBillingService;
beforeEach(() => {
apiService = mock<ApiService>();
billingApiService = mock<BillingApiServiceAbstraction>();
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
i18nService = mock<I18nService>();
organizationApiService = mock<OrganizationApiService>();
syncService = mock<SyncService>();
configService = mock<ConfigService>();
sut = new OrganizationBillingService(
apiService,
billingApiService,
keyService,
encryptService,
i18nService,
organizationApiService,
syncService,
configService,
);
});
afterEach(() => {
return jest.resetAllMocks();
});
describe("isBreadcrumbingPoliciesEnabled", () => {
it("returns false when feature flag is disabled", async () => {
configService.getFeatureFlag$.mockReturnValue(of(false));
const org = {
isProviderUser: false,
canEditSubscription: true,
productTierType: ProductTierType.Teams,
} as Organization;
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
expect(actual).toBe(false);
expect(configService.getFeatureFlag$).toHaveBeenCalledWith(
FeatureFlag.PM12276_BreadcrumbEventLogs,
);
});
it("returns false when organization belongs to a provider", async () => {
configService.getFeatureFlag$.mockReturnValue(of(true));
const org = {
isProviderUser: true,
canEditSubscription: true,
productTierType: ProductTierType.Teams,
} as Organization;
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
expect(actual).toBe(false);
});
it("returns false when cannot edit subscription", async () => {
configService.getFeatureFlag$.mockReturnValue(of(true));
const org = {
isProviderUser: false,
canEditSubscription: false,
productTierType: ProductTierType.Teams,
} as Organization;
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
expect(actual).toBe(false);
});
it.each([
["Teams", ProductTierType.Teams],
["TeamsStarter", ProductTierType.TeamsStarter],
])("returns true when all conditions are met with %s tier", async (_, productTierType) => {
configService.getFeatureFlag$.mockReturnValue(of(true));
const org = {
isProviderUser: false,
canEditSubscription: true,
productTierType: productTierType,
} as Organization;
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
expect(actual).toBe(true);
expect(configService.getFeatureFlag$).toHaveBeenCalledWith(
FeatureFlag.PM12276_BreadcrumbEventLogs,
);
});
it("returns false when product tier is not supported", async () => {
configService.getFeatureFlag$.mockReturnValue(of(true));
const org = {
isProviderUser: false,
canEditSubscription: true,
productTierType: ProductTierType.Enterprise,
} as Organization;
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
expect(actual).toBe(false);
});
it("handles all conditions false correctly", async () => {
configService.getFeatureFlag$.mockReturnValue(of(false));
const org = {
isProviderUser: true,
canEditSubscription: false,
productTierType: ProductTierType.Free,
} as Organization;
const actual = await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
expect(actual).toBe(false);
});
it("verifies feature flag is only called once", async () => {
configService.getFeatureFlag$.mockReturnValue(of(false));
const org = {
isProviderUser: false,
canEditSubscription: true,
productTierType: ProductTierType.Teams,
} as Organization;
await firstValueFrom(sut.isBreadcrumbingPoliciesEnabled$(org));
expect(configService.getFeatureFlag$).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -1,5 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Observable, of, switchMap } from "rxjs";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { KeyService } from "@bitwarden/key-management";
import { ApiService } from "../../abstractions/api.service";
@@ -20,7 +25,7 @@ import {
PlanInformation,
SubscriptionInformation,
} from "../abstractions";
import { PlanType } from "../enums";
import { PlanType, ProductTierType } from "../enums";
import { OrganizationNoPaymentMethodCreateRequest } from "../models/request/organization-no-payment-method-create-request";
import { PaymentSourceResponse } from "../models/response/payment-source.response";
@@ -40,6 +45,7 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
private i18nService: I18nService,
private organizationApiService: OrganizationApiService,
private syncService: SyncService,
private configService: ConfigService,
) {}
async getPaymentSource(organizationId: string): Promise<PaymentSourceResponse> {
@@ -220,4 +226,29 @@ export class OrganizationBillingService implements OrganizationBillingServiceAbs
this.setPaymentInformation(request, subscription.payment);
await this.billingApiService.restartSubscription(organizationId, request);
}
isBreadcrumbingPoliciesEnabled$(organization: Organization): Observable<boolean> {
if (organization === null || organization === undefined) {
return of(false);
}
return this.configService.getFeatureFlag$(FeatureFlag.PM12276_BreadcrumbEventLogs).pipe(
switchMap((featureFlagEnabled) => {
if (!featureFlagEnabled) {
return of(false);
}
if (organization.isProviderUser || !organization.canEditSubscription) {
return of(false);
}
const supportedProducts = [ProductTierType.Teams, ProductTierType.TeamsStarter];
const isSupportedProduct = supportedProducts.some(
(product) => product === organization.productTierType,
);
return of(isSupportedProduct);
}),
);
}
}

View File

@@ -47,7 +47,7 @@ export class EncryptServiceImplementation implements EncryptService {
}
if (this.blockType0) {
if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) {
if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) {
throw new Error("Type 0 encryption is not supported.");
}
}
@@ -84,7 +84,7 @@ export class EncryptServiceImplementation implements EncryptService {
}
if (this.blockType0) {
if (key.encType === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) {
if (key.inner().type === EncryptionType.AesCbc256_B64 || key.key.byteLength < 64) {
throw new Error("Type 0 encryption is not supported.");
}
}
@@ -124,7 +124,7 @@ export class EncryptServiceImplementation implements EncryptService {
if (encString.encryptionType !== innerKey.type) {
this.logDecryptError(
"Key encryption type does not match payload encryption type",
key.encType,
innerKey.type,
encString.encryptionType,
decryptContext,
);
@@ -148,7 +148,7 @@ export class EncryptServiceImplementation implements EncryptService {
if (!macsEqual) {
this.logMacFailed(
"decryptToUtf8 MAC comparison failed. Key or payload has changed.",
key.encType,
innerKey.type,
encString.encryptionType,
decryptContext,
);
@@ -191,7 +191,7 @@ export class EncryptServiceImplementation implements EncryptService {
if (encThing.encryptionType !== inner.type) {
this.logDecryptError(
"Encryption key type mismatch",
key.encType,
inner.type,
encThing.encryptionType,
decryptContext,
);
@@ -200,19 +200,23 @@ export class EncryptServiceImplementation implements EncryptService {
if (inner.type === EncryptionType.AesCbc256_HmacSha256_B64) {
if (encThing.macBytes == null) {
this.logDecryptError("Mac missing", key.encType, encThing.encryptionType, decryptContext);
this.logDecryptError("Mac missing", inner.type, encThing.encryptionType, decryptContext);
return null;
}
const macData = new Uint8Array(encThing.ivBytes.byteLength + encThing.dataBytes.byteLength);
macData.set(new Uint8Array(encThing.ivBytes), 0);
macData.set(new Uint8Array(encThing.dataBytes), encThing.ivBytes.byteLength);
const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256");
const computedMac = await this.cryptoFunctionService.hmac(
macData,
inner.authenticationKey,
"sha256",
);
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
if (!macsMatch) {
this.logMacFailed(
"MAC comparison failed. Key or payload has changed.",
key.encType,
inner.type,
encThing.encryptionType,
decryptContext,
);
@@ -222,14 +226,14 @@ export class EncryptServiceImplementation implements EncryptService {
return await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
key.encKey,
inner.encryptionKey,
"cbc",
);
} else if (inner.type === EncryptionType.AesCbc256_B64) {
return await this.cryptoFunctionService.aesDecrypt(
encThing.dataBytes,
encThing.ivBytes,
key.encKey,
inner.encryptionKey,
"cbc",
);
}

View File

@@ -6,7 +6,10 @@ import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import {
Aes256CbcHmacKey,
SymmetricCryptoKey,
} from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { CsprngArray } from "@bitwarden/common/types/csprng";
import { makeStaticByteArray } from "../../../../spec";
@@ -64,6 +67,10 @@ describe("EncryptService", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.key = makeStaticByteArray(32);
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: mock32Key.key,
});
await expect(encryptService.encrypt(null!, key)).rejects.toThrow(
"Type 0 encryption is not supported.",
@@ -146,6 +153,10 @@ describe("EncryptService", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const mock32Key = mock<SymmetricCryptoKey>();
mock32Key.key = makeStaticByteArray(32);
mock32Key.inner.mockReturnValue({
type: 0,
encryptionKey: mock32Key.key,
});
await expect(encryptService.encryptToBytes(plainValue, key)).rejects.toThrow(
"Type 0 encryption is not supported.",
@@ -228,7 +239,7 @@ describe("EncryptService", () => {
expect(cryptoFunctionService.aesDecrypt).toBeCalledWith(
expect.toEqualBuffer(encBuffer.dataBytes),
expect.toEqualBuffer(encBuffer.ivBytes),
expect.toEqualBuffer(key.encKey),
expect.toEqualBuffer(key.inner().encryptionKey),
"cbc",
);
@@ -249,7 +260,7 @@ describe("EncryptService", () => {
expect(cryptoFunctionService.aesDecrypt).toBeCalledWith(
expect.toEqualBuffer(encBuffer.dataBytes),
expect.toEqualBuffer(encBuffer.ivBytes),
expect.toEqualBuffer(key.encKey),
expect.toEqualBuffer(key.inner().encryptionKey),
"cbc",
);
@@ -267,7 +278,7 @@ describe("EncryptService", () => {
expect(cryptoFunctionService.hmac).toBeCalledWith(
expect.toEqualBuffer(expectedMacData),
key.macKey,
(key.inner() as Aes256CbcHmacKey).authenticationKey,
"sha256",
);

View File

@@ -1,6 +1,7 @@
import * as argon2 from "argon2-browser";
import * as forge from "node-forge";
import { EncryptionType } from "../../../platform/enums";
import { Utils } from "../../../platform/misc/utils";
import {
CbcDecryptParameters,
@@ -247,37 +248,26 @@ export class WebCryptoFunctionService implements CryptoFunctionService {
mac: string | null,
key: SymmetricCryptoKey,
): CbcDecryptParameters<string> {
const p = {} as CbcDecryptParameters<string>;
if (key.meta != null) {
p.encKey = key.meta.encKeyByteString;
p.macKey = key.meta.macKeyByteString;
const innerKey = key.inner();
if (innerKey.type === EncryptionType.AesCbc256_B64) {
return {
iv: forge.util.decode64(iv),
data: forge.util.decode64(data),
encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(),
} as CbcDecryptParameters<string>;
} else if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const macData = forge.util.decode64(iv) + forge.util.decode64(data);
return {
iv: forge.util.decode64(iv),
data: forge.util.decode64(data),
encKey: forge.util.createBuffer(innerKey.encryptionKey).getBytes(),
macKey: forge.util.createBuffer(innerKey.authenticationKey).getBytes(),
mac: forge.util.decode64(mac!),
macData,
} as CbcDecryptParameters<string>;
} else {
throw new Error("Unsupported encryption type.");
}
if (p.encKey == null) {
p.encKey = forge.util.decode64(key.encKeyB64);
}
p.data = forge.util.decode64(data);
p.iv = forge.util.decode64(iv);
p.macData = p.iv + p.data;
if (p.macKey == null && key.macKeyB64 != null) {
p.macKey = forge.util.decode64(key.macKeyB64);
}
if (mac != null) {
p.mac = forge.util.decode64(mac);
}
// cache byte string keys for later
if (key.meta == null) {
key.meta = {};
}
if (key.meta.encKeyByteString == null) {
key.meta.encKeyByteString = p.encKey;
}
if (p.macKey != null && key.meta.macKeyByteString == null) {
key.meta.macKeyByteString = p.macKey;
}
return p;
}
aesDecryptFast({

View File

@@ -209,9 +209,8 @@ export class DeviceTrustService implements DeviceTrustServiceAbstraction {
devices.data
.filter((device) => device.isTrusted)
.map(async (device) => {
const deviceWithKeys = await this.devicesApiService.getDeviceKeys(device.identifier);
const publicKey = await this.encryptService.decryptToBytes(
deviceWithKeys.encryptedPublicKey,
new EncString(device.encryptedPublicKey),
oldUserKey,
);

View File

@@ -252,7 +252,9 @@ describe("KeyConnectorService", () => {
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
const masterKey = getMockMasterKey();
masterPasswordService.masterKeySubject.next(masterKey);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
);
jest.spyOn(keyConnectorService, "getManagingOrganization").mockResolvedValue(organization);
jest.spyOn(apiService, "postUserKeyToKeyConnector").mockResolvedValue();
@@ -273,7 +275,9 @@ describe("KeyConnectorService", () => {
// Arrange
const organization = organizationData(true, true, "https://key-connector-url.com", 2, false);
const masterKey = getMockMasterKey();
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
);
const error = new Error("Failed to post user key to key connector");
organizationService.organizations$.mockReturnValue(of([organization]));

View File

@@ -95,7 +95,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
userId ??= (await firstValueFrom(this.accountService.activeAccount$))?.id;
const organization = await this.getManagingOrganization(userId);
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
);
try {
await this.apiService.postUserKeyToKeyConnector(
@@ -157,7 +159,9 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
await this.tokenService.getEmail(),
kdfConfig,
);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
);
await this.masterPasswordService.setMasterKey(masterKey, userId);
const userKey = await this.keyService.makeUserKey(masterKey);

View File

@@ -1 +1,5 @@
// See https://contributing.bitwarden.com/architecture/clients/data-model/#view for proper use.
// View models represent the decrypted state of a corresponding Domain model.
// They typically match the Domain model but contains a decrypted string for any EncString fields.
// Don't use this to represent arbitrary component view data as that isn't what it is for.
export class View {}

View File

@@ -706,4 +706,73 @@ describe("Utils Service", () => {
});
});
});
describe("fromUtf8ToB64(...)", () => {
const originalIsNode = Utils.isNode;
afterEach(() => {
Utils.isNode = originalIsNode;
});
runInBothEnvironments("should handle empty string", () => {
const str = Utils.fromUtf8ToB64("");
expect(str).toBe("");
});
runInBothEnvironments("should convert a normal b64 string", () => {
const str = Utils.fromUtf8ToB64(asciiHelloWorld);
expect(str).toBe(b64HelloWorldString);
});
runInBothEnvironments("should convert various special characters", () => {
const cases = [
{ input: "»", output: "wrs=" },
{ input: "¦", output: "wqY=" },
{ input: "£", output: "wqM=" },
{ input: "é", output: "w6k=" },
{ input: "ö", output: "w7Y=" },
{ input: "»»", output: "wrvCuw==" },
];
cases.forEach((c) => {
const utfStr = c.input;
const str = Utils.fromUtf8ToB64(utfStr);
expect(str).toBe(c.output);
});
});
});
describe("fromB64ToUtf8(...)", () => {
const originalIsNode = Utils.isNode;
afterEach(() => {
Utils.isNode = originalIsNode;
});
runInBothEnvironments("should handle empty string", () => {
const str = Utils.fromB64ToUtf8("");
expect(str).toBe("");
});
runInBothEnvironments("should convert a normal b64 string", () => {
const str = Utils.fromB64ToUtf8(b64HelloWorldString);
expect(str).toBe(asciiHelloWorld);
});
runInBothEnvironments("should handle various special characters", () => {
const cases = [
{ input: "wrs=", output: "»" },
{ input: "wqY=", output: "¦" },
{ input: "wqM=", output: "£" },
{ input: "w6k=", output: "é" },
{ input: "w7Y=", output: "ö" },
{ input: "wrvCuw==", output: "»»" },
];
cases.forEach((c) => {
const b64Str = c.input;
const str = Utils.fromB64ToUtf8(b64Str);
expect(str).toBe(c.output);
});
});
});
});

View File

@@ -233,7 +233,7 @@ export class Utils {
if (Utils.isNode) {
return Buffer.from(utfStr, "utf8").toString("base64");
} else {
return decodeURIComponent(escape(Utils.global.btoa(utfStr)));
return BufferLib.from(utfStr, "utf8").toString("base64");
}
}
@@ -245,7 +245,7 @@ export class Utils {
if (Utils.isNode) {
return Buffer.from(b64Str, "base64").toString("utf8");
} else {
return decodeURIComponent(escape(Utils.global.atob(b64Str)));
return BufferLib.from(b64Str, "base64").toString("utf8");
}
}

View File

@@ -2,7 +2,7 @@ import { makeStaticByteArray } from "../../../../spec";
import { EncryptionType } from "../../enums";
import { Utils } from "../../misc/utils";
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
import { Aes256CbcHmacKey, SymmetricCryptoKey } from "./symmetric-crypto-key";
describe("SymmetricCryptoKey", () => {
it("errors if no key", () => {
@@ -19,13 +19,8 @@ describe("SymmetricCryptoKey", () => {
const cryptoKey = new SymmetricCryptoKey(key);
expect(cryptoKey).toEqual({
encKey: key,
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: EncryptionType.AesCbc256_B64,
key: key,
keyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
macKey: null,
macKeyB64: undefined,
innerKey: {
type: EncryptionType.AesCbc256_B64,
encryptionKey: key,
@@ -38,14 +33,9 @@ describe("SymmetricCryptoKey", () => {
const cryptoKey = new SymmetricCryptoKey(key);
expect(cryptoKey).toEqual({
encKey: key.slice(0, 32),
encKeyB64: "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
encType: EncryptionType.AesCbc256_HmacSha256_B64,
key: key,
keyB64:
"AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+Pw==",
macKey: key.slice(32, 64),
macKeyB64: "ICEiIyQlJicoKSorLC0uLzAxMjM0NTY3ODk6Ozw9Pj8=",
innerKey: {
type: EncryptionType.AesCbc256_HmacSha256_B64,
encryptionKey: key.slice(0, 32),
@@ -86,8 +76,8 @@ describe("SymmetricCryptoKey", () => {
expect(actual).toEqual({
type: EncryptionType.AesCbc256_HmacSha256_B64,
encryptionKey: key.encKey,
authenticationKey: key.macKey,
encryptionKey: key.inner().encryptionKey,
authenticationKey: (key.inner() as Aes256CbcHmacKey).authenticationKey,
});
});
@@ -95,7 +85,7 @@ describe("SymmetricCryptoKey", () => {
const key = new SymmetricCryptoKey(makeStaticByteArray(32));
const actual = key.toEncoded();
expect(actual).toEqual(key.encKey);
expect(actual).toEqual(key.inner().encryptionKey);
});
it("toEncoded returns encoded key for AesCbc256_HmacSha256_B64", () => {

View File

@@ -25,15 +25,7 @@ export class SymmetricCryptoKey {
private innerKey: Aes256CbcHmacKey | Aes256CbcKey;
key: Uint8Array;
encKey: Uint8Array;
macKey?: Uint8Array;
encType: EncryptionType;
keyB64: string;
encKeyB64: string;
macKeyB64: string;
meta: any;
/**
* @param key The key in one of the permitted serialization formats
@@ -48,30 +40,16 @@ export class SymmetricCryptoKey {
type: EncryptionType.AesCbc256_B64,
encryptionKey: key,
};
this.encType = EncryptionType.AesCbc256_B64;
this.key = key;
this.keyB64 = Utils.fromBufferToB64(this.key);
this.encKey = key;
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
this.macKey = null;
this.macKeyB64 = undefined;
this.keyB64 = this.toBase64();
} else if (key.byteLength === 64) {
this.innerKey = {
type: EncryptionType.AesCbc256_HmacSha256_B64,
encryptionKey: key.slice(0, 32),
authenticationKey: key.slice(32),
};
this.encType = EncryptionType.AesCbc256_HmacSha256_B64;
this.key = key;
this.keyB64 = Utils.fromBufferToB64(this.key);
this.encKey = key.slice(0, 32);
this.encKeyB64 = Utils.fromBufferToB64(this.encKey);
this.macKey = key.slice(32);
this.macKeyB64 = Utils.fromBufferToB64(this.macKey);
this.keyB64 = this.toBase64();
} else {
throw new Error(`Unsupported encType/key length ${key.byteLength}`);
}

View File

@@ -134,7 +134,7 @@ class MyWebPushConnector implements WebPushConnector {
private async pushManagerSubscribe(key: string) {
return await this.serviceWorkerRegistration.pushManager.subscribe({
userVisibleOnly: true,
userVisibleOnly: false,
applicationServerKey: key,
});
}

View File

@@ -1,19 +1,23 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, HostBinding, Input } from "@angular/core";
import { Component, Input } from "@angular/core";
import { DomSanitizer, SafeHtml } from "@angular/platform-browser";
import { Icon, isIcon } from "./icon";
@Component({
selector: "bit-icon",
host: {
"[attr.aria-hidden]": "!ariaLabel",
"[attr.aria-label]": "ariaLabel",
"[innerHtml]": "innerHtml",
},
template: ``,
standalone: true,
})
export class BitIconComponent {
innerHtml: SafeHtml | null = null;
@Input() set icon(icon: Icon) {
if (!isIcon(icon)) {
this.innerHtml = "";
return;
}
@@ -21,7 +25,7 @@ export class BitIconComponent {
this.innerHtml = this.domSanitizer.bypassSecurityTrustHtml(svg);
}
@HostBinding() innerHtml: SafeHtml;
@Input() ariaLabel: string | undefined = undefined;
constructor(private domSanitizer: DomSanitizer) {}
}

View File

@@ -98,9 +98,19 @@ import * as stories from "./icon.stories";
```
- **HTML:**
> NOTE: SVG icons are treated as decorative by default and will be `aria-hidden` unless an
> `ariaLabel` is explicitly provided to the `<bit-icon>` component
```html
<bit-icon [icon]="Icons.ExampleIcon"></bit-icon>
```
With `ariaLabel`
```html
<bit-icon [icon]="Icons.ExampleIcon" [ariaLabel]="Your custom label text here"></bit-icon>
```
8. **Ensure your SVG renders properly** according to Figma in both light and dark modes on a client
which supports multiple style modes.

View File

@@ -26,5 +26,9 @@ export const Default: Story = {
mapping: GenericIcons,
control: { type: "select" },
},
ariaLabel: {
control: "text",
description: "the text used by a screen reader to describe the icon",
},
},
};

View File

@@ -7,3 +7,4 @@ export { LockComponentService, UnlockOptions } from "./lock/services/lock-compon
export { KeyRotationTrustInfoComponent } from "./key-rotation/key-rotation-trust-info.component";
export { AccountRecoveryTrustComponent } from "./trust/account-recovery-trust.component";
export { EmergencyAccessTrustComponent } from "./trust/emergency-access-trust.component";
export { RemovePasswordComponent } from "./key-connector/remove-password.component";

View File

@@ -3,6 +3,7 @@ import * as crypto from "crypto";
import * as forge from "node-forge";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptionType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import {
CbcDecryptParameters,
@@ -172,24 +173,33 @@ export class NodeCryptoFunctionService implements CryptoFunctionService {
mac: string | null,
key: SymmetricCryptoKey,
): CbcDecryptParameters<Uint8Array> {
const p = {} as CbcDecryptParameters<Uint8Array>;
p.encKey = key.encKey;
p.data = Utils.fromB64ToArray(data);
p.iv = Utils.fromB64ToArray(iv);
const dataBytes = Utils.fromB64ToArray(data);
const ivBytes = Utils.fromB64ToArray(iv);
const macBytes = mac != null ? Utils.fromB64ToArray(mac) : null;
const macData = new Uint8Array(p.iv.byteLength + p.data.byteLength);
macData.set(new Uint8Array(p.iv), 0);
macData.set(new Uint8Array(p.data), p.iv.byteLength);
p.macData = macData;
const innerKey = key.inner();
if (key.macKey != null) {
p.macKey = key.macKey;
if (innerKey.type === EncryptionType.AesCbc256_B64) {
return {
iv: ivBytes,
data: dataBytes,
encKey: innerKey.encryptionKey,
} as CbcDecryptParameters<Uint8Array>;
} else if (innerKey.type === EncryptionType.AesCbc256_HmacSha256_B64) {
const macData = new Uint8Array(ivBytes.byteLength + dataBytes.byteLength);
macData.set(new Uint8Array(ivBytes), 0);
macData.set(new Uint8Array(dataBytes), ivBytes.byteLength);
return {
iv: ivBytes,
data: dataBytes,
mac: macBytes,
macData: macData,
encKey: innerKey.encryptionKey,
macKey: innerKey.authenticationKey,
} as CbcDecryptParameters<Uint8Array>;
} else {
throw new Error("Unsupported encryption type");
}
if (mac != null) {
p.mac = Utils.fromB64ToArray(mac);
}
return p;
}
async aesDecryptFast({

View File

@@ -220,7 +220,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
this.exportForm.controls.vaultSelector.valueChanges
.pipe(takeUntil(this.destroy$))
.subscribe(([value]) => {
.subscribe((value) => {
this.organizationId = value !== "myVault" ? value : undefined;
this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip");

8
package-lock.json generated
View File

@@ -191,11 +191,11 @@
},
"apps/browser": {
"name": "@bitwarden/browser",
"version": "2025.3.2"
"version": "2025.4.0"
},
"apps/cli": {
"name": "@bitwarden/cli",
"version": "2025.3.0",
"version": "2025.4.0",
"license": "SEE LICENSE IN LICENSE.txt",
"dependencies": {
"@koa/multer": "3.0.2",
@@ -231,7 +231,7 @@
},
"apps/desktop": {
"name": "@bitwarden/desktop",
"version": "2025.4.1",
"version": "2025.4.2",
"hasInstallScript": true,
"license": "GPL-3.0"
},
@@ -245,7 +245,7 @@
},
"apps/web": {
"name": "@bitwarden/web-vault",
"version": "2025.4.0"
"version": "2025.4.1"
},
"libs/admin-console": {
"name": "@bitwarden/admin-console",