mirror of
https://github.com/bitwarden/browser
synced 2026-02-11 05:53:42 +00:00
Merge remote-tracking branch 'origin/km/browser-integration-supported-platform-check' into km/tmp-biometrics-fixed
This commit is contained in:
41
.github/workflows/brew-bump-desktop.yml
vendored
41
.github/workflows/brew-bump-desktop.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: Bump Desktop Cask
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- desktop-v**
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
update-desktop-cask:
|
||||
name: Update Bitwarden Desktop Cask
|
||||
runs-on: macos-13
|
||||
steps:
|
||||
- name: Login to Azure
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||
|
||||
- name: Retrieve secrets
|
||||
id: retrieve-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: "bitwarden-ci"
|
||||
secrets: "brew-bump-workflow-pat"
|
||||
|
||||
- name: Update Homebrew cask
|
||||
uses: macauley/action-homebrew-bump-cask@445c42390d790569d938f9068d01af39ca030feb # v1.0.0
|
||||
with:
|
||||
# Required, custom GitHub access token with the 'public_repo' and 'workflow' scopes
|
||||
token: ${{ steps.retrieve-secrets.outputs.brew-bump-workflow-pat }}
|
||||
org: bitwarden
|
||||
tap: Homebrew/homebrew-cask
|
||||
cask: bitwarden
|
||||
tag: ${{ github.ref }}
|
||||
revision: ${{ github.sha }}
|
||||
force: true
|
||||
dryrun: true
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ToastOptions,
|
||||
ToastService,
|
||||
} from "@bitwarden/components";
|
||||
import { BiometricStateService } from "@bitwarden/key-management";
|
||||
|
||||
import { PopupCompactModeService } from "../platform/popup/layout/popup-compact-mode.service";
|
||||
import { PopupViewCacheService } from "../platform/popup/view-cache/popup-view-cache.service";
|
||||
@@ -64,6 +65,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
private toastService: ToastService,
|
||||
private accountService: AccountService,
|
||||
private animationControlService: AnimationControlService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -134,6 +136,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
} else if (msg.command === "reloadProcess") {
|
||||
if (this.platformUtilsService.isSafari()) {
|
||||
window.setTimeout(() => {
|
||||
this.biometricStateService.updateLastProcessReload();
|
||||
window.location.reload();
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
@@ -361,20 +361,17 @@ describe("VaultPopupItemsService", () => {
|
||||
});
|
||||
|
||||
describe("deletedCiphers$", () => {
|
||||
it("should return deleted ciphers", (done) => {
|
||||
const ciphers = [
|
||||
{ id: "1", type: CipherType.Login, name: "Login 1", isDeleted: true },
|
||||
{ id: "2", type: CipherType.Login, name: "Login 2", isDeleted: true },
|
||||
{ id: "3", type: CipherType.Login, name: "Login 3", isDeleted: true },
|
||||
{ id: "4", type: CipherType.Login, name: "Login 4", isDeleted: false },
|
||||
] as CipherView[];
|
||||
it("should return deleted ciphers", async () => {
|
||||
const deletedCipher = new CipherView();
|
||||
deletedCipher.deletedDate = new Date();
|
||||
const ciphers = [new CipherView(), new CipherView(), new CipherView(), deletedCipher];
|
||||
|
||||
cipherServiceMock.getAllDecrypted.mockResolvedValue(ciphers);
|
||||
|
||||
service.deletedCiphers$.subscribe((deletedCiphers) => {
|
||||
expect(deletedCiphers.length).toBe(3);
|
||||
done();
|
||||
});
|
||||
(cipherServiceMock.ciphers$ as BehaviorSubject<any>).next(null);
|
||||
|
||||
const deletedCiphers = await firstValueFrom(service.deletedCiphers$);
|
||||
expect(deletedCiphers.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -247,8 +247,28 @@ export class VaultPopupItemsService {
|
||||
/**
|
||||
* Observable that contains the list of ciphers that have been deleted.
|
||||
*/
|
||||
deletedCiphers$: Observable<CipherView[]> = this._allDecryptedCiphers$.pipe(
|
||||
map((ciphers) => ciphers.filter((c) => c.isDeleted)),
|
||||
deletedCiphers$: Observable<PopupCipherView[]> = this._allDecryptedCiphers$.pipe(
|
||||
switchMap((ciphers) =>
|
||||
combineLatest([
|
||||
this.organizationService.organizations$,
|
||||
this.collectionService.decryptedCollections$,
|
||||
]).pipe(
|
||||
map(([organizations, collections]) => {
|
||||
const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org]));
|
||||
const collectionMap = Object.fromEntries(collections.map((col) => [col.id, col]));
|
||||
return ciphers
|
||||
.filter((c) => c.isDeleted)
|
||||
.map(
|
||||
(cipher) =>
|
||||
new PopupCipherView(
|
||||
cipher,
|
||||
cipher.collectionIds?.map((colId) => collectionMap[colId as CollectionId]),
|
||||
orgMap[cipher.organizationId as OrganizationId],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
shareReplay({ refCount: false, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
|
||||
@@ -13,8 +13,23 @@
|
||||
[appA11yTitle]="'viewItemTitle' | i18n: cipher.name"
|
||||
(click)="onViewCipher(cipher)"
|
||||
>
|
||||
<app-vault-icon slot="start" [cipher]="cipher"></app-vault-icon>
|
||||
<div slot="start" class="tw-justify-start tw-w-7 tw-flex">
|
||||
<app-vault-icon [cipher]="cipher"></app-vault-icon>
|
||||
</div>
|
||||
<span data-testid="item-name">{{ cipher.name }}</span>
|
||||
<i
|
||||
*ngIf="cipher.organizationId"
|
||||
appOrgIcon
|
||||
[tierType]="cipher.organization.productTierType"
|
||||
[size]="'small'"
|
||||
[appA11yTitle]="orgIconTooltip(cipher)"
|
||||
></i>
|
||||
<i
|
||||
*ngIf="cipher.hasAttachments"
|
||||
class="bwi bwi-paperclip bwi-sm"
|
||||
[appA11yTitle]="'attachments' | i18n"
|
||||
></i>
|
||||
<span slot="secondary">{{ cipher.subTitle }}</span>
|
||||
</button>
|
||||
<ng-container slot="end" *ngIf="cipher.edit">
|
||||
<bit-item-action>
|
||||
|
||||
@@ -23,9 +23,12 @@ import {
|
||||
import {
|
||||
CanDeleteCipherDirective,
|
||||
DecryptionFailureDialogComponent,
|
||||
OrgIconDirective,
|
||||
PasswordRepromptService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { PopupCipherView } from "../../views/popup-cipher.view";
|
||||
|
||||
@Component({
|
||||
selector: "app-trash-list-items-container",
|
||||
templateUrl: "trash-list-items-container.component.html",
|
||||
@@ -39,6 +42,7 @@ import {
|
||||
CanDeleteCipherDirective,
|
||||
MenuModule,
|
||||
IconButtonModule,
|
||||
OrgIconDirective,
|
||||
TypographyModule,
|
||||
DecryptionFailureDialogComponent,
|
||||
],
|
||||
@@ -49,7 +53,7 @@ export class TrashListItemsContainerComponent {
|
||||
* The list of trashed items to display.
|
||||
*/
|
||||
@Input()
|
||||
ciphers: CipherView[] = [];
|
||||
ciphers: PopupCipherView[] = [];
|
||||
|
||||
@Input()
|
||||
headerText: string;
|
||||
@@ -64,6 +68,17 @@ export class TrashListItemsContainerComponent {
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* The tooltip text for the organization icon for ciphers that belong to an organization.
|
||||
*/
|
||||
orgIconTooltip(cipher: PopupCipherView) {
|
||||
if (cipher.collectionIds.length > 1) {
|
||||
return this.i18nService.t("nCollections", cipher.collectionIds.length);
|
||||
}
|
||||
|
||||
return cipher.collections[0]?.name;
|
||||
}
|
||||
|
||||
async restore(cipher: CipherView) {
|
||||
try {
|
||||
await this.cipherService.restoreWithServer(cipher.id);
|
||||
|
||||
@@ -8,7 +8,6 @@ import { VaultIcons } from "@bitwarden/vault";
|
||||
import { PopOutComponent } from "../../../platform/popup/components/pop-out.component";
|
||||
import { PopupHeaderComponent } from "../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../platform/popup/layout/popup-page.component";
|
||||
import { VaultListItemsContainerComponent } from "../components/vault-v2/vault-list-items-container/vault-list-items-container.component";
|
||||
import { VaultPopupItemsService } from "../services/vault-popup-items.service";
|
||||
|
||||
import { TrashListItemsContainerComponent } from "./trash-list-items-container/trash-list-items-container.component";
|
||||
@@ -22,7 +21,6 @@ import { TrashListItemsContainerComponent } from "./trash-list-items-container/t
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopOutComponent,
|
||||
VaultListItemsContainerComponent,
|
||||
TrashListItemsContainerComponent,
|
||||
CalloutModule,
|
||||
NoItemsModule,
|
||||
|
||||
@@ -5,6 +5,7 @@ config.content = [
|
||||
"./src/**/*.{html,ts}",
|
||||
"../../libs/components/src/**/*.{html,ts}",
|
||||
"../../libs/auth/src/**/*.{html,ts}",
|
||||
"../../libs/key-management/src/**/*.{html,ts}",
|
||||
"../../libs/vault/src/**/*.{html,ts}",
|
||||
"../../libs/angular/src/**/*.{html,ts}",
|
||||
"../../libs/vault/src/**/*.{html,ts}",
|
||||
|
||||
10
apps/desktop/desktop_native/Cargo.lock
generated
10
apps/desktop/desktop_native/Cargo.lock
generated
@@ -916,7 +916,6 @@ dependencies = [
|
||||
"pin-project",
|
||||
"pkcs8",
|
||||
"rand",
|
||||
"retry",
|
||||
"rsa",
|
||||
"russh-cryptovec",
|
||||
"scopeguard",
|
||||
@@ -2388,15 +2387,6 @@ version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "retry"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9166d72162de3575f950507683fac47e30f6f2c3836b71b7fbc61aa517c9c5f4"
|
||||
dependencies = [
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsa"
|
||||
version = "0.9.6"
|
||||
|
||||
@@ -6,18 +6,16 @@ version = "0.0.0"
|
||||
publish = false
|
||||
|
||||
[features]
|
||||
default = ["sys"]
|
||||
manual_test = []
|
||||
|
||||
sys = [
|
||||
default = [
|
||||
"dep:widestring",
|
||||
"dep:windows",
|
||||
"dep:core-foundation",
|
||||
"dep:security-framework",
|
||||
"dep:security-framework-sys",
|
||||
"dep:zbus",
|
||||
"dep:zbus_polkit",
|
||||
"dep:zbus_polkit"
|
||||
]
|
||||
manual_test = []
|
||||
|
||||
[dependencies]
|
||||
aes = "=0.8.4"
|
||||
@@ -36,7 +34,6 @@ futures = "=0.3.31"
|
||||
interprocess = { version = "=2.2.1", features = ["tokio"] }
|
||||
log = "=0.4.22"
|
||||
rand = "=0.8.5"
|
||||
retry = "=2.0.0"
|
||||
russh-cryptovec = "=0.7.3"
|
||||
scopeguard = "=1.2.0"
|
||||
sha2 = "=0.10.8"
|
||||
|
||||
@@ -3,12 +3,16 @@ use anyhow::{anyhow, Result};
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
#[cfg_attr(target_os = "linux", path = "unix.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
mod biometric;
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
|
||||
pub use biometric::Biometric;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod windows_focus;
|
||||
|
||||
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::crypto::{self, CipherString};
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use std::{ffi::c_void, str::FromStr};
|
||||
use std::{
|
||||
ffi::c_void,
|
||||
str::FromStr,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
|
||||
use rand::RngCore;
|
||||
use retry::delay::Fixed;
|
||||
use sha2::{Digest, Sha256};
|
||||
use windows::{
|
||||
core::{factory, h, s, HSTRING},
|
||||
core::{factory, h, HSTRING},
|
||||
Foundation::IAsyncOperation,
|
||||
Security::{
|
||||
Credentials::{
|
||||
@@ -14,17 +17,7 @@ use windows::{
|
||||
},
|
||||
Cryptography::CryptographicBuffer,
|
||||
},
|
||||
Win32::{
|
||||
Foundation::HWND,
|
||||
System::WinRT::IUserConsentVerifierInterop,
|
||||
UI::{
|
||||
Input::KeyboardAndMouse::{
|
||||
keybd_event, GetAsyncKeyState, SetFocus, KEYEVENTF_EXTENDEDKEY, KEYEVENTF_KEYUP,
|
||||
VK_MENU,
|
||||
},
|
||||
WindowsAndMessaging::{FindWindowA, SetForegroundWindow},
|
||||
},
|
||||
},
|
||||
Win32::{Foundation::HWND, System::WinRT::IUserConsentVerifierInterop},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -32,7 +25,10 @@ use crate::{
|
||||
crypto::CipherString,
|
||||
};
|
||||
|
||||
use super::{decrypt, encrypt};
|
||||
use super::{
|
||||
decrypt, encrypt,
|
||||
windows_focus::{focus_security_prompt, set_focus},
|
||||
};
|
||||
|
||||
/// The Windows OS implementation of the biometric trait.
|
||||
pub struct Biometric {}
|
||||
@@ -103,8 +99,22 @@ impl super::BiometricTrait for Biometric {
|
||||
|
||||
let challenge_buffer = CryptographicBuffer::CreateFromByteArray(&challenge)?;
|
||||
let async_operation = result.Credential()?.RequestSignAsync(&challenge_buffer)?;
|
||||
focus_security_prompt()?;
|
||||
let signature = async_operation.get()?;
|
||||
focus_security_prompt();
|
||||
|
||||
let done = Arc::new(AtomicBool::new(false));
|
||||
let done_clone = done.clone();
|
||||
let _ = std::thread::spawn(move || loop {
|
||||
if !done_clone.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
focus_security_prompt();
|
||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
let signature = async_operation.get();
|
||||
done.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
let signature = signature?;
|
||||
|
||||
if signature.Status()? != KeyCredentialStatus::Success {
|
||||
return Err(anyhow!("Failed to sign data"));
|
||||
@@ -168,57 +178,6 @@ fn random_challenge() -> [u8; 16] {
|
||||
challenge
|
||||
}
|
||||
|
||||
/// Searches for a window that looks like a security prompt and set it as focused.
|
||||
///
|
||||
/// Gives up after 1.5 seconds with a delay of 500ms between each try.
|
||||
fn focus_security_prompt() -> Result<()> {
|
||||
unsafe fn try_find_and_set_focus(
|
||||
class_name: windows::core::PCSTR,
|
||||
) -> retry::OperationResult<(), ()> {
|
||||
let hwnd = unsafe { FindWindowA(class_name, None) };
|
||||
if let Ok(hwnd) = hwnd {
|
||||
set_focus(hwnd);
|
||||
return retry::OperationResult::Ok(());
|
||||
}
|
||||
retry::OperationResult::Retry(())
|
||||
}
|
||||
|
||||
let class_name = s!("Credential Dialog Xaml Host");
|
||||
retry::retry_with_index(Fixed::from_millis(500), |current_try| {
|
||||
if current_try > 3 {
|
||||
return retry::OperationResult::Err(());
|
||||
}
|
||||
|
||||
unsafe { try_find_and_set_focus(class_name) }
|
||||
})
|
||||
.map_err(|_| anyhow!("Failed to find security prompt"))
|
||||
}
|
||||
|
||||
fn set_focus(window: HWND) {
|
||||
let mut pressed = false;
|
||||
|
||||
unsafe {
|
||||
// Simulate holding down Alt key to bypass windows limitations
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getasynckeystate#return-value
|
||||
// The most significant bit indicates if the key is currently being pressed. This means the
|
||||
// value will be negative if the key is pressed.
|
||||
if GetAsyncKeyState(VK_MENU.0 as i32) >= 0 {
|
||||
pressed = true;
|
||||
keybd_event(VK_MENU.0 as u8, 0, KEYEVENTF_EXTENDEDKEY, 0);
|
||||
}
|
||||
let _ = SetForegroundWindow(window);
|
||||
let _ = SetFocus(window);
|
||||
if pressed {
|
||||
keybd_event(
|
||||
VK_MENU.0 as u8,
|
||||
0,
|
||||
KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP,
|
||||
0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
use windows::{
|
||||
core::s,
|
||||
Win32::{
|
||||
Foundation::HWND,
|
||||
UI::{
|
||||
Input::KeyboardAndMouse::SetFocus,
|
||||
WindowsAndMessaging::{FindWindowA, SetForegroundWindow},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/// Searches for a window that looks like a security prompt and set it as focused.
|
||||
/// Only works when the process has permission to foreground, either by being in foreground
|
||||
/// Or by being given foreground permission https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setforegroundwindow#remarks
|
||||
pub fn focus_security_prompt() {
|
||||
let class_name = s!("Credential Dialog Xaml Host");
|
||||
let hwnd = unsafe { FindWindowA(class_name, None) };
|
||||
if let Ok(hwnd) = hwnd {
|
||||
set_focus(hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_focus(window: HWND) {
|
||||
unsafe {
|
||||
let _ = SetForegroundWindow(window);
|
||||
let _ = SetFocus(window);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,10 @@
|
||||
pub mod autofill;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod biometric;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod clipboard;
|
||||
pub mod crypto;
|
||||
pub mod error;
|
||||
pub mod ipc;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod password;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod powermonitor;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod process_isolation;
|
||||
#[cfg(feature = "sys")]
|
||||
pub mod ssh_agent;
|
||||
|
||||
@@ -1,209 +1,132 @@
|
||||
const { existsSync, readFileSync } = require('fs')
|
||||
const { join } = require('path')
|
||||
const { existsSync } = require("fs");
|
||||
const { join } = require("path");
|
||||
|
||||
const { platform, arch } = process
|
||||
const { platform, arch } = process;
|
||||
|
||||
let nativeBinding = null
|
||||
let localFileExisted = false
|
||||
let loadError = null
|
||||
let nativeBinding = null;
|
||||
let localFileExisted = false;
|
||||
let loadError = null;
|
||||
|
||||
function isMusl() {
|
||||
// For Node 10
|
||||
if (!process.report || typeof process.report.getReport !== 'function') {
|
||||
try {
|
||||
return readFileSync('/usr/bin/ldd', 'utf8').includes('musl')
|
||||
} catch (e) {
|
||||
return true
|
||||
function loadFirstAvailable(localFiles, nodeModule) {
|
||||
for (const localFile of localFiles) {
|
||||
if (existsSync(join(__dirname, localFile))) {
|
||||
return require(`./${localFile}`);
|
||||
}
|
||||
} else {
|
||||
const { glibcVersionRuntime } = process.report.getReport().header
|
||||
return !glibcVersionRuntime
|
||||
}
|
||||
|
||||
require(nodeModule);
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case 'android':
|
||||
case "android":
|
||||
switch (arch) {
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(join(__dirname, 'desktop_napi.android-arm64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.android-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-android-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(join(__dirname, 'desktop_napi.android-arm-eabi.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.android-arm-eabi.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-android-arm-eabi')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case "arm64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.android-arm64.node"],
|
||||
"@bitwarden/desktop-napi-android-arm64",
|
||||
);
|
||||
break;
|
||||
case "arm":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.android-arm.node"],
|
||||
"@bitwarden/desktop-napi-android-arm",
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Android ${arch}`)
|
||||
throw new Error(`Unsupported architecture on Android ${arch}`);
|
||||
}
|
||||
break
|
||||
case 'win32':
|
||||
break;
|
||||
case "win32":
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.win32-x64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.win32-x64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-win32-x64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'ia32':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.win32-ia32-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.win32-ia32-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-win32-ia32-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.win32-arm64-msvc.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.win32-arm64-msvc.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-win32-arm64-msvc')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case "x64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.win32-x64-msvc.node"],
|
||||
"@bitwarden/desktop-napi-win32-x64-msvc",
|
||||
);
|
||||
break;
|
||||
case "ia32":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.win32-ia32-msvc.node"],
|
||||
"@bitwarden/desktop-napi-win32-ia32-msvc",
|
||||
);
|
||||
break;
|
||||
case "arm64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.win32-arm64-msvc.node"],
|
||||
"@bitwarden/desktop-napi-win32-arm64-msvc",
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
||||
throw new Error(`Unsupported architecture on Windows: ${arch}`);
|
||||
}
|
||||
break
|
||||
case 'darwin':
|
||||
break;
|
||||
case "darwin":
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(join(__dirname, 'desktop_napi.darwin-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.darwin-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-darwin-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.darwin-arm64.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.darwin-arm64.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-darwin-arm64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case "x64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.darwin-x64.node"],
|
||||
"@bitwarden/desktop-napi-darwin-x64",
|
||||
);
|
||||
break;
|
||||
case "arm64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.darwin-arm64.node"],
|
||||
"@bitwarden/desktop-napi-darwin-arm64",
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
||||
throw new Error(`Unsupported architecture on macOS: ${arch}`);
|
||||
}
|
||||
break
|
||||
case 'freebsd':
|
||||
if (arch !== 'x64') {
|
||||
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
||||
}
|
||||
localFileExisted = existsSync(join(__dirname, 'desktop_napi.freebsd-x64.node'))
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.freebsd-x64.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-freebsd-x64')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'linux':
|
||||
break;
|
||||
case "freebsd":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.freebsd-x64.node"],
|
||||
"@bitwarden/desktop-napi-freebsd-x64",
|
||||
);
|
||||
break;
|
||||
case "linux":
|
||||
switch (arch) {
|
||||
case 'x64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.linux-x64-musl.node')
|
||||
)
|
||||
case "x64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-x64-musl.node", "desktop_napi.linux-x64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-x64-musl",
|
||||
);
|
||||
break;
|
||||
case "arm64":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-arm64-musl.node", "desktop_napi.linux-arm64-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm64-musl",
|
||||
);
|
||||
break;
|
||||
case "arm":
|
||||
nativeBinding = loadFirstAvailable(
|
||||
["desktop_napi.linux-arm-musl.node", "desktop_napi.linux-arm-gnu.node"],
|
||||
"@bitwarden/desktop-napi-linux-arm-musl",
|
||||
);
|
||||
localFileExisted = existsSync(join(__dirname, "desktop_napi.linux-arm-gnueabihf.node"));
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.linux-x64-musl.node')
|
||||
nativeBinding = require("./desktop_napi.linux-arm-gnueabihf.node");
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-linux-x64-musl')
|
||||
nativeBinding = require("@bitwarden/desktop-napi-linux-arm-gnueabihf");
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
loadError = e;
|
||||
}
|
||||
break
|
||||
case 'arm64':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.linux-arm64-musl.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.linux-arm64-musl.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-linux-arm64-musl')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
case 'arm':
|
||||
localFileExisted = existsSync(
|
||||
join(__dirname, 'desktop_napi.linux-arm-gnueabihf.node')
|
||||
)
|
||||
try {
|
||||
if (localFileExisted) {
|
||||
nativeBinding = require('./desktop_napi.linux-arm-gnueabihf.node')
|
||||
} else {
|
||||
nativeBinding = require('@bitwarden/desktop-napi-linux-arm-gnueabihf')
|
||||
}
|
||||
} catch (e) {
|
||||
loadError = e
|
||||
}
|
||||
break
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
||||
throw new Error(`Unsupported architecture on Linux: ${arch}`);
|
||||
}
|
||||
break
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
||||
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`);
|
||||
}
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadError) {
|
||||
throw loadError
|
||||
throw loadError;
|
||||
}
|
||||
throw new Error(`Failed to load native binding`)
|
||||
throw new Error(`Failed to load native binding`);
|
||||
}
|
||||
|
||||
module.exports = nativeBinding
|
||||
module.exports = nativeBinding;
|
||||
|
||||
@@ -8,7 +8,7 @@ publish = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "=1.0.94"
|
||||
desktop_core = { path = "../core", default-features = false }
|
||||
desktop_core = { path = "../core" }
|
||||
futures = "=0.3.31"
|
||||
log = "=0.4.22"
|
||||
simplelog = "=0.12.2"
|
||||
|
||||
@@ -5,6 +5,9 @@ use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use log::*;
|
||||
use tokio_util::codec::LengthDelimitedCodec;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
embed_plist::embed_info_plist!("../../../resources/info.desktop_proxy.plist");
|
||||
|
||||
@@ -49,6 +52,9 @@ fn init_logging(log_path: &Path, console_level: LevelFilter, file_level: LevelFi
|
||||
///
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
#[cfg(target_os = "windows")]
|
||||
let should_foreground = windows::allow_foreground();
|
||||
|
||||
let sock_path = desktop_core::ipc::path("bitwarden");
|
||||
|
||||
let log_path = {
|
||||
@@ -142,6 +148,9 @@ async fn main() {
|
||||
|
||||
// Listen to stdin and send messages to ipc processor.
|
||||
msg = stdin.next() => {
|
||||
#[cfg(target_os = "windows")]
|
||||
should_foreground.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||
|
||||
match msg {
|
||||
Some(Ok(msg)) => {
|
||||
let m = String::from_utf8(msg.to_vec()).unwrap();
|
||||
|
||||
23
apps/desktop/desktop_native/proxy/src/windows.rs
Normal file
23
apps/desktop/desktop_native/proxy/src/windows.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
pub fn allow_foreground() -> Arc<AtomicBool> {
|
||||
let should_foreground = Arc::new(AtomicBool::new(false));
|
||||
let should_foreground_clone = should_foreground.clone();
|
||||
let _ = std::thread::spawn(move || loop {
|
||||
if !should_foreground_clone.load(Ordering::Relaxed) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
continue;
|
||||
}
|
||||
should_foreground_clone.store(false, Ordering::Relaxed);
|
||||
|
||||
for _ in 0..60 {
|
||||
desktop_core::biometric::windows_focus::focus_security_prompt();
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000));
|
||||
}
|
||||
});
|
||||
|
||||
should_foreground
|
||||
}
|
||||
@@ -650,7 +650,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
|
||||
const skipSupportedPlatformCheck =
|
||||
ipc.platform.allowBrowserintegrationOverride || ipc.platform.isDev;
|
||||
|
||||
if (skipSupportedPlatformCheck) {
|
||||
if (!skipSupportedPlatformCheck) {
|
||||
if (
|
||||
ipc.platform.deviceType === DeviceType.MacOsDesktop &&
|
||||
!this.platformUtilsService.isMacAppStore()
|
||||
|
||||
@@ -45,6 +45,8 @@ export class SshAgentService implements OnDestroy {
|
||||
SSH_VAULT_UNLOCK_REQUEST_TIMEOUT = 60_000;
|
||||
SSH_REQUEST_UNLOCK_POLLING_INTERVAL = 100;
|
||||
|
||||
private isFeatureFlagEnabled = false;
|
||||
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
@@ -65,18 +67,19 @@ export class SshAgentService implements OnDestroy {
|
||||
.getFeatureFlag$(FeatureFlag.SSHAgent)
|
||||
.pipe(
|
||||
concatMap(async (enabled) => {
|
||||
if (enabled && !(await ipc.platform.sshAgent.isLoaded())) {
|
||||
return this.initSshAgent();
|
||||
this.isFeatureFlagEnabled = enabled;
|
||||
if (!(await ipc.platform.sshAgent.isLoaded()) && enabled) {
|
||||
await ipc.platform.sshAgent.init();
|
||||
}
|
||||
}),
|
||||
takeUntil(this.destroy$),
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
await this.initListeners();
|
||||
}
|
||||
|
||||
private async initSshAgent() {
|
||||
await ipc.platform.sshAgent.init();
|
||||
|
||||
private async initListeners() {
|
||||
this.messageListener
|
||||
.messages$(new CommandDefinition("sshagent.signrequest"))
|
||||
.pipe(
|
||||
@@ -179,18 +182,30 @@ export class SshAgentService implements OnDestroy {
|
||||
|
||||
this.accountService.activeAccount$.pipe(skip(1), takeUntil(this.destroy$)).subscribe({
|
||||
next: (account) => {
|
||||
if (!this.isFeatureFlagEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.info("Active account changed, clearing SSH keys");
|
||||
ipc.platform.sshAgent
|
||||
.clearKeys()
|
||||
.catch((e) => this.logService.error("Failed to clear SSH keys", e));
|
||||
},
|
||||
error: (e: unknown) => {
|
||||
if (!this.isFeatureFlagEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.error("Error in active account observable", e);
|
||||
ipc.platform.sshAgent
|
||||
.clearKeys()
|
||||
.catch((e) => this.logService.error("Failed to clear SSH keys", e));
|
||||
},
|
||||
complete: () => {
|
||||
if (!this.isFeatureFlagEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.info("Active account observable completed, clearing SSH keys");
|
||||
ipc.platform.sshAgent
|
||||
.clearKeys()
|
||||
@@ -204,11 +219,23 @@ export class SshAgentService implements OnDestroy {
|
||||
])
|
||||
.pipe(
|
||||
concatMap(async ([, enabled]) => {
|
||||
if (!this.isFeatureFlagEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
await ipc.platform.sshAgent.clearKeys();
|
||||
return;
|
||||
}
|
||||
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
const authStatus = await firstValueFrom(
|
||||
this.authService.authStatusFor$(activeAccount.id),
|
||||
);
|
||||
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ciphers = await this.cipherService.getAllDecrypted();
|
||||
if (ciphers == null) {
|
||||
await ipc.platform.sshAgent.lock();
|
||||
|
||||
@@ -5,6 +5,7 @@ config.content = [
|
||||
"./src/**/*.{html,ts}",
|
||||
"../../libs/components/src/**/*.{html,ts}",
|
||||
"../../libs/auth/src/**/*.{html,ts}",
|
||||
"../../libs/key-management/src/**/*.{html,ts}",
|
||||
"../../libs/angular/src/**/*.{html,ts}",
|
||||
"../../libs/vault/src/**/*.{html,ts,mdx}",
|
||||
];
|
||||
|
||||
@@ -23,14 +23,15 @@
|
||||
<bit-tab [label]="'role' | i18n">
|
||||
<ng-container *ngIf="!editMode">
|
||||
<p bitTypography="body1">{{ "inviteUserDesc" | i18n }}</p>
|
||||
<bit-form-field>
|
||||
<bit-form-field *ngIf="remainingSeats$ | async as remainingSeats">
|
||||
<bit-label>{{ "email" | i18n }}</bit-label>
|
||||
<input id="emails" type="text" appAutoFocus bitInput formControlName="emails" />
|
||||
<bit-hint>{{
|
||||
"inviteMultipleEmailDesc"
|
||||
| i18n
|
||||
: (organization.productTierType === ProductTierType.TeamsStarter ? "10" : "20")
|
||||
<bit-hint *ngIf="remainingSeats > 1; else singleSeat">{{
|
||||
"inviteMultipleEmailDesc" | i18n: remainingSeats
|
||||
}}</bit-hint>
|
||||
<ng-template #singleSeat>
|
||||
<bit-hint>{{ "inviteSingleEmailDesc" | i18n: remainingSeats }}</bit-hint>
|
||||
</ng-template>
|
||||
</bit-form-field>
|
||||
</ng-container>
|
||||
<bit-radio-group formControlName="type">
|
||||
|
||||
@@ -94,6 +94,7 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
PermissionMode = PermissionMode;
|
||||
showNoMasterPasswordWarning = false;
|
||||
isOnSecretsManagerStandalone: boolean;
|
||||
remainingSeats$: Observable<number>;
|
||||
|
||||
protected organization$: Observable<Organization>;
|
||||
protected collectionAccessItems: AccessItemView[] = [];
|
||||
@@ -260,6 +261,10 @@ export class MemberDialogComponent implements OnDestroy {
|
||||
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
this.remainingSeats$ = this.organization$.pipe(
|
||||
map((organization) => organization.seats - this.params.numConfirmedMembers),
|
||||
);
|
||||
}
|
||||
|
||||
private setFormValidators(organization: Organization) {
|
||||
|
||||
@@ -231,6 +231,20 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "syncOrganizationCollectionSettingChanged": {
|
||||
const { organizationId, limitCollectionCreation, limitCollectionDeletion } = message;
|
||||
const organizations = await firstValueFrom(this.organizationService.organizations$);
|
||||
const organization = organizations.find((org) => org.id === organizationId);
|
||||
|
||||
if (organization) {
|
||||
await this.organizationService.upsert({
|
||||
...organization,
|
||||
limitCollectionCreation: limitCollectionCreation,
|
||||
limitCollectionDeletion: limitCollectionDeletion,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -117,14 +117,7 @@ describe("EmergencyAccessService", () => {
|
||||
const granteeId = "grantee-id";
|
||||
const mockUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
|
||||
const mockPublicKeyB64 = "some-public-key-in-base64";
|
||||
|
||||
// const publicKey = Utils.fromB64ToArray(publicKeyB64);
|
||||
|
||||
const mockUserPublicKeyResponse = new UserKeyResponse({
|
||||
UserId: granteeId,
|
||||
PublicKey: mockPublicKeyB64,
|
||||
});
|
||||
const publicKey = new Uint8Array(64);
|
||||
|
||||
const mockUserPublicKeyEncryptedUserKey = new EncString(
|
||||
EncryptionType.AesCbc256_HmacSha256_B64,
|
||||
@@ -132,14 +125,13 @@ describe("EmergencyAccessService", () => {
|
||||
);
|
||||
|
||||
keyService.getUserKey.mockResolvedValueOnce(mockUserKey);
|
||||
apiService.getUserPublicKey.mockResolvedValueOnce(mockUserPublicKeyResponse);
|
||||
|
||||
encryptService.rsaEncrypt.mockResolvedValueOnce(mockUserPublicKeyEncryptedUserKey);
|
||||
|
||||
emergencyAccessApiService.postEmergencyAccessConfirm.mockResolvedValueOnce();
|
||||
|
||||
// Act
|
||||
await emergencyAccessService.confirm(id, granteeId);
|
||||
await emergencyAccessService.confirm(id, granteeId, publicKey);
|
||||
|
||||
// Assert
|
||||
expect(emergencyAccessApiService.postEmergencyAccessConfirm).toHaveBeenCalledWith(id, {
|
||||
|
||||
@@ -153,14 +153,13 @@ export class EmergencyAccessService
|
||||
* Intended for grantor.
|
||||
* @param id emergency access id
|
||||
* @param token secret token provided in email
|
||||
* @param publicKey public key of grantee
|
||||
*/
|
||||
async confirm(id: string, granteeId: string) {
|
||||
async confirm(id: string, granteeId: string, publicKey: Uint8Array): Promise<void> {
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
if (!userKey) {
|
||||
throw new Error("No user key found");
|
||||
}
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(granteeId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
|
||||
try {
|
||||
this.logService.debug(
|
||||
|
||||
@@ -4,10 +4,8 @@ import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, OnInit, Inject } from "@angular/core";
|
||||
import { FormBuilder } from "@angular/forms";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
@@ -21,6 +19,8 @@ type EmergencyAccessConfirmDialogData = {
|
||||
userId: string;
|
||||
/** traces a unique emergency request */
|
||||
emergencyAccessId: string;
|
||||
/** user public key */
|
||||
publicKey: Uint8Array;
|
||||
};
|
||||
@Component({
|
||||
selector: "emergency-access-confirm",
|
||||
@@ -36,7 +36,6 @@ export class EmergencyAccessConfirmComponent implements OnInit {
|
||||
constructor(
|
||||
@Inject(DIALOG_DATA) protected params: EmergencyAccessConfirmDialogData,
|
||||
private formBuilder: FormBuilder,
|
||||
private apiService: ApiService,
|
||||
private keyService: KeyService,
|
||||
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||
private logService: LogService,
|
||||
@@ -45,13 +44,12 @@ export class EmergencyAccessConfirmComponent implements OnInit {
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(this.params.userId);
|
||||
if (publicKeyResponse != null) {
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
const fingerprint = await this.keyService.getFingerprint(this.params.userId, publicKey);
|
||||
if (fingerprint != null) {
|
||||
this.fingerprint = fingerprint.join("-");
|
||||
}
|
||||
const fingerprint = await this.keyService.getFingerprint(
|
||||
this.params.userId,
|
||||
this.params.publicKey,
|
||||
);
|
||||
if (fingerprint != null) {
|
||||
this.fingerprint = fingerprint.join("-");
|
||||
}
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||
import { lastValueFrom, Observable, firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
@@ -13,6 +14,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { DialogService, ToastService } from "@bitwarden/components";
|
||||
|
||||
import { EmergencyAccessService } from "../../emergency-access";
|
||||
@@ -70,6 +72,7 @@ export class EmergencyAccessComponent implements OnInit {
|
||||
billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
protected organizationManagementPreferencesService: OrganizationManagementPreferencesService,
|
||||
private toastService: ToastService,
|
||||
private apiService: ApiService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.canAccessPremium$ = this.accountService.activeAccount$.pipe(
|
||||
@@ -147,6 +150,9 @@ export class EmergencyAccessComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKeyResponse = await this.apiService.getUserPublicKey(contact.granteeId);
|
||||
const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey);
|
||||
|
||||
const autoConfirm = await firstValueFrom(
|
||||
this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$,
|
||||
);
|
||||
@@ -156,11 +162,12 @@ export class EmergencyAccessComponent implements OnInit {
|
||||
name: this.userNamePipe.transform(contact),
|
||||
emergencyAccessId: contact.id,
|
||||
userId: contact?.granteeId,
|
||||
publicKey,
|
||||
},
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === EmergencyAccessConfirmDialogResult.Confirmed) {
|
||||
await this.emergencyAccessService.confirm(contact.id, contact.granteeId);
|
||||
await this.emergencyAccessService.confirm(contact.id, contact.granteeId, publicKey);
|
||||
updateUser();
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -171,7 +178,11 @@ export class EmergencyAccessComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
this.actionPromise = this.emergencyAccessService.confirm(contact.id, contact.granteeId);
|
||||
this.actionPromise = this.emergencyAccessService.confirm(
|
||||
contact.id,
|
||||
contact.granteeId,
|
||||
publicKey,
|
||||
);
|
||||
await this.actionPromise;
|
||||
updateUser();
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>{{ "deviceListDescription" | i18n }}</p>
|
||||
<p>{{ "deviceListDescriptionTemp" | i18n }}</p>
|
||||
|
||||
<div *ngIf="loading" class="tw-flex tw-justify-center tw-items-center tw-p-4">
|
||||
<i class="bwi bwi-spinner bwi-spin tw-text-2xl" aria-hidden="true"></i>
|
||||
@@ -63,13 +63,14 @@
|
||||
}}</span>
|
||||
</td>
|
||||
<td bitCell>{{ row.firstLogin | date: "medium" }}</td>
|
||||
<td bitCell>
|
||||
<!-- <td bitCell>
|
||||
<button
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
[bitMenuTriggerFor]="optionsMenu"
|
||||
></button>
|
||||
<bit-menu #optionsMenu>
|
||||
Remove device button to be re-added later when we have per device session de-authentication.
|
||||
<button
|
||||
type="button"
|
||||
bitMenuItem
|
||||
@@ -82,7 +83,7 @@
|
||||
</span>
|
||||
</button>
|
||||
</bit-menu>
|
||||
</td>
|
||||
</td> -->
|
||||
</ng-template>
|
||||
</bit-table-scroll>
|
||||
</bit-container>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Meta, moduleMetadata, StoryObj } from "@storybook/angular";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
@@ -22,6 +23,14 @@ export default {
|
||||
moduleMetadata({
|
||||
imports: [JslibModule, BadgeModule],
|
||||
providers: [
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: {
|
||||
activeAccount$: of({
|
||||
id: "123",
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: I18nService,
|
||||
useFactory: () => {
|
||||
@@ -39,7 +48,7 @@ export default {
|
||||
{
|
||||
provide: BillingAccountProfileStateService,
|
||||
useValue: {
|
||||
hasPremiumFromAnySource$: of(false),
|
||||
hasPremiumFromAnySource$: () => of(false),
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { TestBed } from "@angular/core/testing";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
import { BehaviorSubject, firstValueFrom, take, timeout } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import {
|
||||
UserDecryptionOptions,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { FakeStateProvider, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
@@ -22,18 +25,20 @@ describe("VaultBannersService", () => {
|
||||
let service: VaultBannersService;
|
||||
const isSelfHost = jest.fn().mockReturnValue(false);
|
||||
const hasPremiumFromAnySource$ = new BehaviorSubject<boolean>(false);
|
||||
const userId = "user-id" as UserId;
|
||||
const userId = Utils.newGuid() as UserId;
|
||||
const fakeStateProvider = new FakeStateProvider(mockAccountServiceWith(userId));
|
||||
const getEmailVerified = jest.fn().mockResolvedValue(true);
|
||||
const hasMasterPassword = jest.fn().mockResolvedValue(true);
|
||||
const getKdfConfig = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600000 });
|
||||
const getLastSync = jest.fn().mockResolvedValue(null);
|
||||
const lastSync$ = new BehaviorSubject<Date | null>(null);
|
||||
const userDecryptionOptions$ = new BehaviorSubject<UserDecryptionOptions>({
|
||||
hasMasterPassword: true,
|
||||
});
|
||||
const kdfConfig$ = new BehaviorSubject({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600000 });
|
||||
const accounts$ = new BehaviorSubject<Record<UserId, AccountInfo>>({
|
||||
[userId]: { email: "test@bitwarden.com", emailVerified: true, name: "name" } as AccountInfo,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
getLastSync.mockClear().mockResolvedValue(new Date("2024-05-14"));
|
||||
lastSync$.next(new Date("2024-05-14"));
|
||||
isSelfHost.mockClear();
|
||||
getEmailVerified.mockClear().mockResolvedValue(true);
|
||||
|
||||
@@ -52,25 +57,27 @@ describe("VaultBannersService", () => {
|
||||
provide: StateProvider,
|
||||
useValue: fakeStateProvider,
|
||||
},
|
||||
{
|
||||
provide: PlatformUtilsService,
|
||||
useValue: { isSelfHost },
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: mockAccountServiceWith(userId),
|
||||
},
|
||||
{
|
||||
provide: TokenService,
|
||||
useValue: { getEmailVerified },
|
||||
},
|
||||
{
|
||||
provide: UserVerificationService,
|
||||
useValue: { hasMasterPassword },
|
||||
useValue: { accounts$ },
|
||||
},
|
||||
{
|
||||
provide: KdfConfigService,
|
||||
useValue: { getKdfConfig },
|
||||
useValue: { getKdfConfig$: () => kdfConfig$ },
|
||||
},
|
||||
{
|
||||
provide: SyncService,
|
||||
useValue: { getLastSync },
|
||||
useValue: { lastSync$: () => lastSync$ },
|
||||
},
|
||||
{
|
||||
provide: UserDecryptionOptionsServiceAbstraction,
|
||||
useValue: {
|
||||
userDecryptionOptionsById$: () => userDecryptionOptions$,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -82,39 +89,38 @@ describe("VaultBannersService", () => {
|
||||
|
||||
describe("Premium", () => {
|
||||
it("waits until sync is completed before showing premium banner", async () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
isSelfHost.mockReturnValue(false);
|
||||
lastSync$.next(null);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
jest.advanceTimersByTime(201);
|
||||
const premiumBanner$ = service.shouldShowPremiumBanner$(userId);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(true);
|
||||
// Should not emit when sync is null
|
||||
await expect(firstValueFrom(premiumBanner$.pipe(take(1), timeout(100)))).rejects.toThrow();
|
||||
|
||||
// Should emit when sync is completed
|
||||
lastSync$.next(new Date("2024-05-14"));
|
||||
expect(await firstValueFrom(premiumBanner$)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show a premium banner for self-hosted users", async () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(false);
|
||||
isSelfHost.mockReturnValue(true);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
jest.advanceTimersByTime(201);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(false);
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$(userId))).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show a premium banner when they have access to premium", async () => {
|
||||
getLastSync.mockResolvedValue(new Date("2024-05-14"));
|
||||
hasPremiumFromAnySource$.next(true);
|
||||
isSelfHost.mockReturnValue(false);
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
jest.advanceTimersByTime(201);
|
||||
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$)).toBe(false);
|
||||
expect(await firstValueFrom(service.shouldShowPremiumBanner$(userId))).toBe(false);
|
||||
});
|
||||
|
||||
describe("dismissing", () => {
|
||||
@@ -125,7 +131,7 @@ describe("VaultBannersService", () => {
|
||||
jest.setSystemTime(date.getTime());
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
await service.dismissBanner(VisibleVaultBanner.Premium);
|
||||
await service.dismissBanner(userId, VisibleVaultBanner.Premium);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -134,7 +140,7 @@ describe("VaultBannersService", () => {
|
||||
|
||||
it("updates state on first dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneWeekLater = new Date("2023-06-15");
|
||||
@@ -148,7 +154,7 @@ describe("VaultBannersService", () => {
|
||||
|
||||
it("updates state on second dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneMonthLater = new Date("2023-07-08");
|
||||
@@ -162,7 +168,7 @@ describe("VaultBannersService", () => {
|
||||
|
||||
it("updates state on third dismiss", async () => {
|
||||
const state = await firstValueFrom(
|
||||
fakeStateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
fakeStateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY).state$,
|
||||
);
|
||||
|
||||
const oneYearLater = new Date("2024-06-08");
|
||||
@@ -178,40 +184,40 @@ describe("VaultBannersService", () => {
|
||||
|
||||
describe("KDFSettings", () => {
|
||||
beforeEach(async () => {
|
||||
hasMasterPassword.mockResolvedValue(true);
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 });
|
||||
userDecryptionOptions$.next({ hasMasterPassword: true });
|
||||
kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 599999 });
|
||||
});
|
||||
|
||||
it("shows low KDF iteration banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(true);
|
||||
expect(await service.shouldShowLowKDFBanner(userId)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not show low KDF iteration banner if KDF type is not PBKDF2_SHA256", async () => {
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.Argon2id, iterations: 600001 });
|
||||
kdfConfig$.next({ kdfType: KdfType.Argon2id, iterations: 600001 });
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||
expect(await service.shouldShowLowKDFBanner(userId)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not show low KDF for iterations about 600,000", async () => {
|
||||
getKdfConfig.mockResolvedValue({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 });
|
||||
kdfConfig$.next({ kdfType: KdfType.PBKDF2_SHA256, iterations: 600001 });
|
||||
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||
expect(await service.shouldShowLowKDFBanner(userId)).toBe(false);
|
||||
});
|
||||
|
||||
it("dismisses low KDF iteration banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(true);
|
||||
expect(await service.shouldShowLowKDFBanner(userId)).toBe(true);
|
||||
|
||||
await service.dismissBanner(VisibleVaultBanner.KDFSettings);
|
||||
await service.dismissBanner(userId, VisibleVaultBanner.KDFSettings);
|
||||
|
||||
expect(await service.shouldShowLowKDFBanner()).toBe(false);
|
||||
expect(await service.shouldShowLowKDFBanner(userId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -228,39 +234,44 @@ describe("VaultBannersService", () => {
|
||||
it("shows outdated browser banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowUpdateBrowserBanner()).toBe(true);
|
||||
expect(await service.shouldShowUpdateBrowserBanner(userId)).toBe(true);
|
||||
});
|
||||
|
||||
it("dismisses outdated browser banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowUpdateBrowserBanner()).toBe(true);
|
||||
expect(await service.shouldShowUpdateBrowserBanner(userId)).toBe(true);
|
||||
|
||||
await service.dismissBanner(VisibleVaultBanner.OutdatedBrowser);
|
||||
await service.dismissBanner(userId, VisibleVaultBanner.OutdatedBrowser);
|
||||
|
||||
expect(await service.shouldShowUpdateBrowserBanner()).toBe(false);
|
||||
expect(await service.shouldShowUpdateBrowserBanner(userId)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("VerifyEmail", () => {
|
||||
beforeEach(async () => {
|
||||
getEmailVerified.mockResolvedValue(false);
|
||||
accounts$.next({
|
||||
[userId]: {
|
||||
...accounts$.value[userId],
|
||||
emailVerified: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("shows verify email banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowVerifyEmailBanner()).toBe(true);
|
||||
expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(true);
|
||||
});
|
||||
|
||||
it("dismisses verify email banner", async () => {
|
||||
service = TestBed.inject(VaultBannersService);
|
||||
|
||||
expect(await service.shouldShowVerifyEmailBanner()).toBe(true);
|
||||
expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(true);
|
||||
|
||||
await service.dismissBanner(VisibleVaultBanner.VerifyEmail);
|
||||
await service.dismissBanner(userId, VisibleVaultBanner.VerifyEmail);
|
||||
|
||||
expect(await service.shouldShowVerifyEmailBanner()).toBe(false);
|
||||
expect(await service.shouldShowVerifyEmailBanner(userId)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import {
|
||||
Subject,
|
||||
Observable,
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
map,
|
||||
mergeMap,
|
||||
take,
|
||||
switchMap,
|
||||
of,
|
||||
} from "rxjs";
|
||||
import { Observable, combineLatest, firstValueFrom, map, filter, mergeMap, take } from "rxjs";
|
||||
|
||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import {
|
||||
StateProvider,
|
||||
ActiveUserState,
|
||||
PREMIUM_BANNER_DISK_LOCAL,
|
||||
BANNERS_DISMISSED_DISK,
|
||||
UserKeyDefinition,
|
||||
SingleUserState,
|
||||
} from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { PBKDF2KdfConfig, KdfConfigService, KdfType } from "@bitwarden/key-management";
|
||||
|
||||
@@ -62,47 +52,25 @@ export const BANNERS_DISMISSED_DISK_KEY = new UserKeyDefinition<SessionBanners[]
|
||||
|
||||
@Injectable()
|
||||
export class VaultBannersService {
|
||||
shouldShowPremiumBanner$: Observable<boolean>;
|
||||
|
||||
private premiumBannerState: ActiveUserState<PremiumBannerReprompt>;
|
||||
private sessionBannerState: ActiveUserState<SessionBanners[]>;
|
||||
|
||||
/**
|
||||
* Emits when the sync service has completed a sync
|
||||
*
|
||||
* This is needed because `hasPremiumFromAnySource$` will emit false until the sync is completed
|
||||
* resulting in the premium banner being shown briefly on startup when the user has access to
|
||||
* premium features.
|
||||
*/
|
||||
private syncCompleted$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private tokenService: TokenService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private accountService: AccountService,
|
||||
private stateProvider: StateProvider,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private syncService: SyncService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.pollUntilSynced();
|
||||
this.premiumBannerState = this.stateProvider.getActive(PREMIUM_BANNER_REPROMPT_KEY);
|
||||
this.sessionBannerState = this.stateProvider.getActive(BANNERS_DISMISSED_DISK_KEY);
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
) {}
|
||||
|
||||
const premiumSources$ = this.accountService.activeAccount$.pipe(
|
||||
take(1),
|
||||
switchMap((account) => {
|
||||
return combineLatest([
|
||||
account
|
||||
? this.billingAccountProfileStateService.hasPremiumFromAnySource$(account.id)
|
||||
: of(false),
|
||||
this.premiumBannerState.state$,
|
||||
]);
|
||||
}),
|
||||
);
|
||||
shouldShowPremiumBanner$(userId: UserId): Observable<boolean> {
|
||||
const premiumBannerState = this.premiumBannerState(userId);
|
||||
const premiumSources$ = combineLatest([
|
||||
this.billingAccountProfileStateService.hasPremiumFromAnySource$(userId),
|
||||
premiumBannerState.state$,
|
||||
]);
|
||||
|
||||
this.shouldShowPremiumBanner$ = this.syncCompleted$.pipe(
|
||||
return this.syncService.lastSync$(userId).pipe(
|
||||
filter((lastSync) => lastSync !== null),
|
||||
take(1), // Wait until the first sync is complete before considering the premium status
|
||||
mergeMap(() => premiumSources$),
|
||||
map(([canAccessPremium, dismissedState]) => {
|
||||
@@ -122,9 +90,9 @@ export class VaultBannersService {
|
||||
}
|
||||
|
||||
/** Returns true when the update browser banner should be shown */
|
||||
async shouldShowUpdateBrowserBanner(): Promise<boolean> {
|
||||
async shouldShowUpdateBrowserBanner(userId: UserId): Promise<boolean> {
|
||||
const outdatedBrowser = window.navigator.userAgent.indexOf("MSIE") !== -1;
|
||||
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||
const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
|
||||
VisibleVaultBanner.OutdatedBrowser,
|
||||
);
|
||||
|
||||
@@ -132,10 +100,12 @@ export class VaultBannersService {
|
||||
}
|
||||
|
||||
/** Returns true when the verify email banner should be shown */
|
||||
async shouldShowVerifyEmailBanner(): Promise<boolean> {
|
||||
const needsVerification = !(await this.tokenService.getEmailVerified());
|
||||
async shouldShowVerifyEmailBanner(userId: UserId): Promise<boolean> {
|
||||
const needsVerification = !(
|
||||
await firstValueFrom(this.accountService.accounts$.pipe(map((accounts) => accounts[userId])))
|
||||
)?.emailVerified;
|
||||
|
||||
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||
const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
|
||||
VisibleVaultBanner.VerifyEmail,
|
||||
);
|
||||
|
||||
@@ -143,12 +113,14 @@ export class VaultBannersService {
|
||||
}
|
||||
|
||||
/** Returns true when the low KDF iteration banner should be shown */
|
||||
async shouldShowLowKDFBanner(): Promise<boolean> {
|
||||
const hasLowKDF = (await this.userVerificationService.hasMasterPassword())
|
||||
? await this.isLowKdfIteration()
|
||||
async shouldShowLowKDFBanner(userId: UserId): Promise<boolean> {
|
||||
const hasLowKDF = (
|
||||
await firstValueFrom(this.userDecryptionOptionsService.userDecryptionOptionsById$(userId))
|
||||
)?.hasMasterPassword
|
||||
? await this.isLowKdfIteration(userId)
|
||||
: false;
|
||||
|
||||
const alreadyDismissed = (await this.getBannerDismissedState()).includes(
|
||||
const alreadyDismissed = (await this.getBannerDismissedState(userId)).includes(
|
||||
VisibleVaultBanner.KDFSettings,
|
||||
);
|
||||
|
||||
@@ -156,11 +128,11 @@ export class VaultBannersService {
|
||||
}
|
||||
|
||||
/** Dismiss the given banner and perform any respective side effects */
|
||||
async dismissBanner(banner: SessionBanners): Promise<void> {
|
||||
async dismissBanner(userId: UserId, banner: SessionBanners): Promise<void> {
|
||||
if (banner === VisibleVaultBanner.Premium) {
|
||||
await this.dismissPremiumBanner();
|
||||
await this.dismissPremiumBanner(userId);
|
||||
} else {
|
||||
await this.sessionBannerState.update((current) => {
|
||||
await this.sessionBannerState(userId).update((current) => {
|
||||
const bannersDismissed = current ?? [];
|
||||
|
||||
return [...bannersDismissed, banner];
|
||||
@@ -168,16 +140,32 @@ export class VaultBannersService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns a SingleUserState for the premium banner reprompt state
|
||||
*/
|
||||
private premiumBannerState(userId: UserId): SingleUserState<PremiumBannerReprompt> {
|
||||
return this.stateProvider.getUser(userId, PREMIUM_BANNER_REPROMPT_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns a SingleUserState for the session banners dismissed state
|
||||
*/
|
||||
private sessionBannerState(userId: UserId): SingleUserState<SessionBanners[]> {
|
||||
return this.stateProvider.getUser(userId, BANNERS_DISMISSED_DISK_KEY);
|
||||
}
|
||||
|
||||
/** Returns banners that have already been dismissed */
|
||||
private async getBannerDismissedState(): Promise<SessionBanners[]> {
|
||||
private async getBannerDismissedState(userId: UserId): Promise<SessionBanners[]> {
|
||||
// `state$` can emit null when a value has not been set yet,
|
||||
// use nullish coalescing to default to an empty array
|
||||
return (await firstValueFrom(this.sessionBannerState.state$)) ?? [];
|
||||
return (await firstValueFrom(this.sessionBannerState(userId).state$)) ?? [];
|
||||
}
|
||||
|
||||
/** Increment dismissal state of the premium banner */
|
||||
private async dismissPremiumBanner(): Promise<void> {
|
||||
await this.premiumBannerState.update((current) => {
|
||||
private async dismissPremiumBanner(userId: UserId): Promise<void> {
|
||||
await this.premiumBannerState(userId).update((current) => {
|
||||
const numberOfDismissals = current?.numberOfDismissals ?? 0;
|
||||
const now = new Date();
|
||||
|
||||
@@ -213,22 +201,11 @@ export class VaultBannersService {
|
||||
});
|
||||
}
|
||||
|
||||
private async isLowKdfIteration() {
|
||||
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||
private async isLowKdfIteration(userId: UserId) {
|
||||
const kdfConfig = await firstValueFrom(this.kdfConfigService.getKdfConfig$(userId));
|
||||
return (
|
||||
kdfConfig.kdfType === KdfType.PBKDF2_SHA256 &&
|
||||
kdfConfig.iterations < PBKDF2KdfConfig.ITERATIONS.defaultValue
|
||||
);
|
||||
}
|
||||
|
||||
/** Poll the `syncService` until a sync is completed */
|
||||
private pollUntilSynced() {
|
||||
const interval = setInterval(async () => {
|
||||
const lastSync = await this.syncService.getLastSync();
|
||||
if (lastSync !== null) {
|
||||
clearInterval(interval);
|
||||
this.syncCompleted$.next();
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,17 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||
import { By } from "@angular/platform-browser";
|
||||
import { RouterTestingModule } from "@angular/router/testing";
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BehaviorSubject, Observable } from "rxjs";
|
||||
|
||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { BannerComponent, BannerModule } from "@bitwarden/components";
|
||||
|
||||
import { VerifyEmailComponent } from "../../../auth/settings/verify-email.component";
|
||||
@@ -21,21 +25,25 @@ describe("VaultBannersComponent", () => {
|
||||
let component: VaultBannersComponent;
|
||||
let fixture: ComponentFixture<VaultBannersComponent>;
|
||||
const premiumBanner$ = new BehaviorSubject<boolean>(false);
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
|
||||
const bannerService = mock<VaultBannersService>({
|
||||
shouldShowPremiumBanner$: premiumBanner$,
|
||||
shouldShowPremiumBanner$: jest.fn((userId$: Observable<UserId>) => premiumBanner$),
|
||||
shouldShowUpdateBrowserBanner: jest.fn(),
|
||||
shouldShowVerifyEmailBanner: jest.fn(),
|
||||
shouldShowLowKDFBanner: jest.fn(),
|
||||
dismissBanner: jest.fn(),
|
||||
});
|
||||
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
|
||||
beforeEach(async () => {
|
||||
bannerService.shouldShowPremiumBanner$ = premiumBanner$;
|
||||
bannerService.shouldShowUpdateBrowserBanner.mockResolvedValue(false);
|
||||
bannerService.shouldShowVerifyEmailBanner.mockResolvedValue(false);
|
||||
bannerService.shouldShowLowKDFBanner.mockResolvedValue(false);
|
||||
|
||||
premiumBanner$.next(false);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
BannerModule,
|
||||
@@ -62,6 +70,10 @@ describe("VaultBannersComponent", () => {
|
||||
provide: TokenService,
|
||||
useValue: mock<TokenService>(),
|
||||
},
|
||||
{
|
||||
provide: AccountService,
|
||||
useValue: accountService,
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideProvider(VaultBannersService, { useValue: bannerService })
|
||||
@@ -135,7 +147,7 @@ describe("VaultBannersComponent", () => {
|
||||
|
||||
dismissButton.dispatchEvent(new Event("click"));
|
||||
|
||||
expect(bannerService.dismissBanner).toHaveBeenCalledWith(banner);
|
||||
expect(bannerService.dismissBanner).toHaveBeenCalledWith(mockUserId, banner);
|
||||
|
||||
expect(component.visibleBanners).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
// @ts-strict-ignore
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
import { Observable } from "rxjs";
|
||||
import { firstValueFrom, map, Observable, switchMap } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { BannerModule } from "@bitwarden/components";
|
||||
|
||||
@@ -26,12 +27,17 @@ export class VaultBannersComponent implements OnInit {
|
||||
VisibleVaultBanner = VisibleVaultBanner;
|
||||
@Input() organizationsPaymentStatus: FreeTrial[] = [];
|
||||
|
||||
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
|
||||
|
||||
constructor(
|
||||
private vaultBannerService: VaultBannersService,
|
||||
private router: Router,
|
||||
private i18nService: I18nService,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.premiumBannerVisible$ = this.vaultBannerService.shouldShowPremiumBanner$;
|
||||
this.premiumBannerVisible$ = this.activeUserId$.pipe(
|
||||
switchMap((userId) => this.vaultBannerService.shouldShowPremiumBanner$(userId)),
|
||||
);
|
||||
}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
@@ -39,7 +45,8 @@ export class VaultBannersComponent implements OnInit {
|
||||
}
|
||||
|
||||
async dismissBanner(banner: VisibleVaultBanner): Promise<void> {
|
||||
await this.vaultBannerService.dismissBanner(banner);
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
await this.vaultBannerService.dismissBanner(activeUserId, banner);
|
||||
|
||||
await this.determineVisibleBanners();
|
||||
}
|
||||
@@ -57,9 +64,12 @@ export class VaultBannersComponent implements OnInit {
|
||||
|
||||
/** Determine which banners should be present */
|
||||
private async determineVisibleBanners(): Promise<void> {
|
||||
const showBrowserOutdated = await this.vaultBannerService.shouldShowUpdateBrowserBanner();
|
||||
const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner();
|
||||
const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner();
|
||||
const activeUserId = await firstValueFrom(this.activeUserId$);
|
||||
|
||||
const showBrowserOutdated =
|
||||
await this.vaultBannerService.shouldShowUpdateBrowserBanner(activeUserId);
|
||||
const showVerifyEmail = await this.vaultBannerService.shouldShowVerifyEmailBanner(activeUserId);
|
||||
const showLowKdf = await this.vaultBannerService.shouldShowLowKDFBanner(activeUserId);
|
||||
|
||||
this.visibleBanners = [
|
||||
showBrowserOutdated ? VisibleVaultBanner.OutdatedBrowser : null,
|
||||
|
||||
@@ -1175,6 +1175,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
||||
|
||||
// Navigate away if we deleted the collection we were viewing
|
||||
if (this.selectedCollection?.node.id === collection.id) {
|
||||
// Clear the cipher cache to clear the deleted collection from the cipher state
|
||||
await this.cipherService.clear();
|
||||
void this.router.navigate([], {
|
||||
queryParams: { collectionId: this.selectedCollection.parent?.node.id ?? null },
|
||||
queryParamsHandling: "merge",
|
||||
|
||||
@@ -3278,6 +3278,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"inviteSingleEmailDesc": {
|
||||
"message": "You have 1 invite remaining."
|
||||
},
|
||||
"userUsingTwoStep": {
|
||||
"message": "This user is using two-step login to protect their account."
|
||||
},
|
||||
@@ -9992,6 +9995,9 @@
|
||||
"deviceListDescription": {
|
||||
"message": "Your account was logged in to each of the devices below. If you do not recognize a device, remove it now."
|
||||
},
|
||||
"deviceListDescriptionTemp": {
|
||||
"message": "Your account was logged in to each of the devices below."
|
||||
},
|
||||
"claimedDomains": {
|
||||
"message": "Claimed domains"
|
||||
},
|
||||
|
||||
@@ -5,6 +5,7 @@ config.content = [
|
||||
"./src/**/*.{html,ts}",
|
||||
"../../libs/components/src/**/*.{html,ts}",
|
||||
"../../libs/auth/src/**/*.{html,ts}",
|
||||
"../../libs/key-management/src/**/*.{html,ts}",
|
||||
"../../libs/vault/src/**/*.{html,ts}",
|
||||
"../../libs/angular/src/**/*.{html,ts}",
|
||||
"../../bitwarden_license/bit-web/src/**/*.{html,ts}",
|
||||
|
||||
@@ -180,10 +180,18 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||
let decUserKey: Uint8Array;
|
||||
|
||||
if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey);
|
||||
decUserKey = await this.encryptService.decryptToBytes(
|
||||
userKey,
|
||||
masterKey,
|
||||
"Content: User Key; Encrypting Key: Master Key",
|
||||
);
|
||||
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const newKey = await this.keyGenerationService.stretchKey(masterKey);
|
||||
decUserKey = await this.encryptService.decryptToBytes(userKey, newKey);
|
||||
decUserKey = await this.encryptService.decryptToBytes(
|
||||
userKey,
|
||||
newKey,
|
||||
"Content: User Key; Encrypting Key: Stretched Master Key",
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unsupported encryption type.");
|
||||
}
|
||||
|
||||
@@ -23,4 +23,5 @@ export enum NotificationType {
|
||||
|
||||
SyncOrganizations = 17,
|
||||
SyncOrganizationStatusChanged = 18,
|
||||
SyncOrganizationCollectionSettingChanged = 19,
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ export class NotificationResponse extends BaseResponse {
|
||||
case NotificationType.SyncOrganizationStatusChanged:
|
||||
this.payload = new OrganizationStatusPushNotification(payload);
|
||||
break;
|
||||
case NotificationType.SyncOrganizationCollectionSettingChanged:
|
||||
this.payload = new OrganizationCollectionSettingChangedPushNotification(payload);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -126,3 +129,17 @@ export class OrganizationStatusPushNotification extends BaseResponse {
|
||||
this.enabled = this.getResponseProperty("Enabled");
|
||||
}
|
||||
}
|
||||
|
||||
export class OrganizationCollectionSettingChangedPushNotification extends BaseResponse {
|
||||
organizationId: string;
|
||||
limitCollectionCreation: boolean;
|
||||
limitCollectionDeletion: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
this.organizationId = this.getResponseProperty("OrganizationId");
|
||||
this.limitCollectionCreation = this.getResponseProperty("LimitCollectionCreation");
|
||||
this.limitCollectionDeletion = this.getResponseProperty("LimitCollectionDeletion");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,32 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||
export abstract class EncryptService {
|
||||
abstract encrypt(plainValue: string | Uint8Array, key: SymmetricCryptoKey): Promise<EncString>;
|
||||
abstract encryptToBytes(plainValue: Uint8Array, key: SymmetricCryptoKey): Promise<EncArrayBuffer>;
|
||||
/**
|
||||
* Decrypts an EncString to a string
|
||||
* @param encString - The EncString to decrypt
|
||||
* @param key - The key to decrypt the EncString with
|
||||
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
|
||||
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
|
||||
* @returns The decrypted string
|
||||
*/
|
||||
abstract decryptToUtf8(
|
||||
encString: EncString,
|
||||
key: SymmetricCryptoKey,
|
||||
decryptContext?: string,
|
||||
decryptTrace?: string,
|
||||
): Promise<string>;
|
||||
abstract decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<Uint8Array>;
|
||||
/**
|
||||
* Decrypts an Encrypted object to a Uint8Array
|
||||
* @param encThing - The Encrypted object to decrypt
|
||||
* @param key - The key to decrypt the Encrypted object with
|
||||
* @param decryptTrace - A string to identify the context of the object being decrypted. This can include: field name, encryption type, cipher id, key type, but should not include
|
||||
* sensitive information like encryption keys or data. This is used for logging when decryption errors occur in order to identify what failed to decrypt
|
||||
* @returns The decrypted Uint8Array
|
||||
*/
|
||||
abstract decryptToBytes(
|
||||
encThing: Encrypted,
|
||||
key: SymmetricCryptoKey,
|
||||
decryptTrace?: string,
|
||||
): Promise<Uint8Array>;
|
||||
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
|
||||
abstract rsaDecrypt(data: EncString, privateKey: Uint8Array): Promise<Uint8Array>;
|
||||
abstract resolveLegacyKey(key: SymmetricCryptoKey, encThing: Encrypted): SymmetricCryptoKey;
|
||||
|
||||
@@ -63,6 +63,7 @@ export default class Domain {
|
||||
map: any,
|
||||
orgId: string,
|
||||
key: SymmetricCryptoKey = null,
|
||||
objectContext: string = "No Domain Context",
|
||||
): Promise<T> {
|
||||
const promises = [];
|
||||
const self: any = this;
|
||||
@@ -78,7 +79,11 @@ export default class Domain {
|
||||
.then(() => {
|
||||
const mapProp = map[theProp] || theProp;
|
||||
if (self[mapProp]) {
|
||||
return self[mapProp].decrypt(orgId, key);
|
||||
return self[mapProp].decrypt(
|
||||
orgId,
|
||||
key,
|
||||
`Property: ${prop}; ObjectContext: ${objectContext}`,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
@@ -114,12 +119,21 @@ export default class Domain {
|
||||
key: SymmetricCryptoKey,
|
||||
encryptService: EncryptService,
|
||||
_: Constructor<TThis> = this.constructor as Constructor<TThis>,
|
||||
objectContext: string = "No Domain Context",
|
||||
): Promise<DecryptedObject<TThis, TEncryptedKeys>> {
|
||||
const promises = [];
|
||||
|
||||
for (const prop of encryptedProperties) {
|
||||
const value = (this as any)[prop] as EncString;
|
||||
promises.push(this.decryptProperty(prop, value, key, encryptService));
|
||||
promises.push(
|
||||
this.decryptProperty(
|
||||
prop,
|
||||
value,
|
||||
key,
|
||||
encryptService,
|
||||
`Property: ${prop.toString()}; ObjectContext: ${objectContext}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const decryptedObjects = await Promise.all(promises);
|
||||
@@ -137,10 +151,11 @@ export default class Domain {
|
||||
value: EncString,
|
||||
key: SymmetricCryptoKey,
|
||||
encryptService: EncryptService,
|
||||
decryptTrace: string,
|
||||
) {
|
||||
let decrypted: string = null;
|
||||
if (value) {
|
||||
decrypted = await value.decryptWithKey(key, encryptService);
|
||||
decrypted = await value.decryptWithKey(key, encryptService, decryptTrace);
|
||||
} else {
|
||||
decrypted = null;
|
||||
}
|
||||
|
||||
@@ -156,21 +156,21 @@ export class EncString implements Encrypted {
|
||||
return EXPECTED_NUM_PARTS_BY_ENCRYPTION_TYPE[encType] === encPieces.length;
|
||||
}
|
||||
|
||||
async decrypt(orgId: string, key: SymmetricCryptoKey = null): Promise<string> {
|
||||
async decrypt(orgId: string, key: SymmetricCryptoKey = null, context?: string): Promise<string> {
|
||||
if (this.decryptedValue != null) {
|
||||
return this.decryptedValue;
|
||||
}
|
||||
|
||||
let keyContext = "provided-key";
|
||||
let decryptTrace = "provided-key";
|
||||
try {
|
||||
if (key == null) {
|
||||
key = await this.getKeyForDecryption(orgId);
|
||||
keyContext = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey";
|
||||
decryptTrace = orgId == null ? `domain-orgkey-${orgId}` : "domain-userkey|masterkey";
|
||||
if (orgId != null) {
|
||||
keyContext = `domain-orgkey-${orgId}`;
|
||||
decryptTrace = `domain-orgkey-${orgId}`;
|
||||
} else {
|
||||
const cryptoService = Utils.getContainerService().getKeyService();
|
||||
keyContext =
|
||||
decryptTrace =
|
||||
(await cryptoService.getUserKey()) == null
|
||||
? "domain-withlegacysupport-masterkey"
|
||||
: "domain-withlegacysupport-userkey";
|
||||
@@ -181,20 +181,28 @@ export class EncString implements Encrypted {
|
||||
}
|
||||
|
||||
const encryptService = Utils.getContainerService().getEncryptService();
|
||||
this.decryptedValue = await encryptService.decryptToUtf8(this, key, keyContext);
|
||||
this.decryptedValue = await encryptService.decryptToUtf8(
|
||||
this,
|
||||
key,
|
||||
decryptTrace == null ? context : `${decryptTrace}${context || ""}`,
|
||||
);
|
||||
} catch (e) {
|
||||
this.decryptedValue = DECRYPT_ERROR;
|
||||
}
|
||||
return this.decryptedValue;
|
||||
}
|
||||
|
||||
async decryptWithKey(key: SymmetricCryptoKey, encryptService: EncryptService) {
|
||||
async decryptWithKey(
|
||||
key: SymmetricCryptoKey,
|
||||
encryptService: EncryptService,
|
||||
decryptTrace: string = "domain-withkey",
|
||||
): Promise<string> {
|
||||
try {
|
||||
if (key == null) {
|
||||
throw new Error("No key to decrypt EncString");
|
||||
}
|
||||
|
||||
this.decryptedValue = await encryptService.decryptToUtf8(this, key, "domain-withkey");
|
||||
this.decryptedValue = await encryptService.decryptToUtf8(this, key, decryptTrace);
|
||||
} catch (e) {
|
||||
this.decryptedValue = DECRYPT_ERROR;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
const macsEqual = await this.cryptoFunctionService.compareFast(fastParams.mac, computedMac);
|
||||
if (!macsEqual) {
|
||||
this.logMacFailed(
|
||||
"[Encrypt service] MAC comparison failed. Key or payload has changed. Key type " +
|
||||
"[Encrypt service] decryptToUtf8 MAC comparison failed. Key or payload has changed. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
"Payload type " +
|
||||
encryptionTypeName(encString.encryptionType) +
|
||||
@@ -128,7 +128,11 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
return await this.cryptoFunctionService.aesDecryptFast(fastParams, "cbc");
|
||||
}
|
||||
|
||||
async decryptToBytes(encThing: Encrypted, key: SymmetricCryptoKey): Promise<Uint8Array> {
|
||||
async decryptToBytes(
|
||||
encThing: Encrypted,
|
||||
key: SymmetricCryptoKey,
|
||||
decryptContext: string = "no context",
|
||||
): Promise<Uint8Array> {
|
||||
if (key == null) {
|
||||
throw new Error("No encryption key provided.");
|
||||
}
|
||||
@@ -145,7 +149,9 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
"[Encrypt service] Key has mac key but payload is missing mac bytes. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType),
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -155,7 +161,9 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
"[Encrypt service] Key encryption type does not match payload encryption type. Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType),
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -167,11 +175,13 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
const computedMac = await this.cryptoFunctionService.hmac(macData, key.macKey, "sha256");
|
||||
if (computedMac === null) {
|
||||
this.logMacFailed(
|
||||
"[Encrypt service] Failed to compute MAC." +
|
||||
"[Encrypt service#decryptToBytes] Failed to compute MAC." +
|
||||
" Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType),
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
@@ -179,11 +189,13 @@ export class EncryptServiceImplementation implements EncryptService {
|
||||
const macsMatch = await this.cryptoFunctionService.compare(encThing.macBytes, computedMac);
|
||||
if (!macsMatch) {
|
||||
this.logMacFailed(
|
||||
"[Encrypt service] MAC comparison failed. Key or payload has changed." +
|
||||
"[Encrypt service#decryptToBytes]: MAC comparison failed. Key or payload has changed." +
|
||||
" Key type " +
|
||||
encryptionTypeName(key.encType) +
|
||||
" Payload type " +
|
||||
encryptionTypeName(encThing.encryptionType),
|
||||
encryptionTypeName(encThing.encryptionType) +
|
||||
" Decrypt context: " +
|
||||
decryptContext,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -227,6 +227,11 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
break;
|
||||
case NotificationType.SyncOrganizationCollectionSettingChanged:
|
||||
if (isAuthenticated) {
|
||||
await this.syncService.fullSync(true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -123,7 +123,12 @@ describe("Send", () => {
|
||||
const view = await send.decrypt();
|
||||
|
||||
expect(text.decrypt).toHaveBeenNthCalledWith(1, "cryptoKey");
|
||||
expect(send.name.decrypt).toHaveBeenNthCalledWith(1, null, "cryptoKey");
|
||||
expect(send.name.decrypt).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
null,
|
||||
"cryptoKey",
|
||||
"Property: name; ObjectContext: No Domain Context",
|
||||
);
|
||||
|
||||
expect(view).toMatchObject({
|
||||
id: "id",
|
||||
|
||||
@@ -43,6 +43,12 @@ export function buildCipherIcon(iconsServerUrl: string, cipher: CipherView, show
|
||||
isWebsite = hostnameUri.indexOf("http") === 0 && hostnameUri.indexOf(".") > -1;
|
||||
}
|
||||
|
||||
if (isWebsite && (hostnameUri.endsWith(".onion") || hostnameUri.endsWith(".i2p"))) {
|
||||
image = null;
|
||||
fallbackImage = "images/bwi-globe.png";
|
||||
break;
|
||||
}
|
||||
|
||||
if (showFavicon && isWebsite) {
|
||||
try {
|
||||
image = `${iconsServerUrl}/${Utils.getHostname(hostnameUri)}/icon.png`;
|
||||
|
||||
@@ -101,7 +101,7 @@ describe("Attachment", () => {
|
||||
it("uses the provided key without depending on KeyService", async () => {
|
||||
const providedKey = mock<SymmetricCryptoKey>();
|
||||
|
||||
await attachment.decrypt(null, providedKey);
|
||||
await attachment.decrypt(null, "", providedKey);
|
||||
|
||||
expect(keyService.getUserKeyWithLegacySupport).not.toHaveBeenCalled();
|
||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, providedKey);
|
||||
@@ -111,7 +111,7 @@ describe("Attachment", () => {
|
||||
const orgKey = mock<OrgKey>();
|
||||
keyService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
|
||||
|
||||
await attachment.decrypt("orgId", null);
|
||||
await attachment.decrypt("orgId", "", null);
|
||||
|
||||
expect(keyService.getOrgKey).toHaveBeenCalledWith("orgId");
|
||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, orgKey);
|
||||
@@ -121,7 +121,7 @@ describe("Attachment", () => {
|
||||
const userKey = mock<UserKey>();
|
||||
keyService.getUserKeyWithLegacySupport.mockResolvedValue(userKey);
|
||||
|
||||
await attachment.decrypt(null, null);
|
||||
await attachment.decrypt(null, "", null);
|
||||
|
||||
expect(keyService.getUserKeyWithLegacySupport).toHaveBeenCalled();
|
||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, userKey);
|
||||
|
||||
@@ -38,7 +38,11 @@ export class Attachment extends Domain {
|
||||
);
|
||||
}
|
||||
|
||||
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<AttachmentView> {
|
||||
async decrypt(
|
||||
orgId: string,
|
||||
context = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<AttachmentView> {
|
||||
const view = await this.decryptObj(
|
||||
new AttachmentView(this),
|
||||
{
|
||||
@@ -46,6 +50,7 @@ export class Attachment extends Domain {
|
||||
},
|
||||
orgId,
|
||||
encKey,
|
||||
"DomainType: Attachment; " + context,
|
||||
);
|
||||
|
||||
if (this.key != null) {
|
||||
|
||||
@@ -37,7 +37,11 @@ export class Card extends Domain {
|
||||
);
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<CardView> {
|
||||
async decrypt(
|
||||
orgId: string,
|
||||
context = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<CardView> {
|
||||
return this.decryptObj(
|
||||
new CardView(),
|
||||
{
|
||||
@@ -50,6 +54,7 @@ export class Card extends Domain {
|
||||
},
|
||||
orgId,
|
||||
encKey,
|
||||
"DomainType: Card; " + context,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,11 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
|
||||
if (this.key != null) {
|
||||
const encryptService = Utils.getContainerService().getEncryptService();
|
||||
const keyBytes = await encryptService.decryptToBytes(this.key, encKey);
|
||||
const keyBytes = await encryptService.decryptToBytes(
|
||||
this.key,
|
||||
encKey,
|
||||
`Cipher Id: ${this.id}; Content: CipherKey; IsEncryptedByOrgKey: ${this.organizationId != null}`,
|
||||
);
|
||||
if (keyBytes == null) {
|
||||
model.name = "[error: cannot decrypt]";
|
||||
model.decryptionFailure = true;
|
||||
@@ -158,19 +162,36 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
model.login = await this.login.decrypt(this.organizationId, bypassValidation, encKey);
|
||||
model.login = await this.login.decrypt(
|
||||
this.organizationId,
|
||||
bypassValidation,
|
||||
`Cipher Id: ${this.id}`,
|
||||
encKey,
|
||||
);
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
model.secureNote = await this.secureNote.decrypt(this.organizationId, encKey);
|
||||
model.secureNote = await this.secureNote.decrypt(
|
||||
this.organizationId,
|
||||
`Cipher Id: ${this.id}`,
|
||||
encKey,
|
||||
);
|
||||
break;
|
||||
case CipherType.Card:
|
||||
model.card = await this.card.decrypt(this.organizationId, encKey);
|
||||
model.card = await this.card.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey);
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
model.identity = await this.identity.decrypt(this.organizationId, encKey);
|
||||
model.identity = await this.identity.decrypt(
|
||||
this.organizationId,
|
||||
`Cipher Id: ${this.id}`,
|
||||
encKey,
|
||||
);
|
||||
break;
|
||||
case CipherType.SshKey:
|
||||
model.sshKey = await this.sshKey.decrypt(this.organizationId, encKey);
|
||||
model.sshKey = await this.sshKey.decrypt(
|
||||
this.organizationId,
|
||||
`Cipher Id: ${this.id}`,
|
||||
encKey,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -181,7 +202,7 @@ export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
await this.attachments.reduce((promise, attachment) => {
|
||||
return promise
|
||||
.then(() => {
|
||||
return attachment.decrypt(this.organizationId, encKey);
|
||||
return attachment.decrypt(this.organizationId, `Cipher Id: ${this.id}`, encKey);
|
||||
})
|
||||
.then((decAttachment) => {
|
||||
attachments.push(decAttachment);
|
||||
|
||||
@@ -61,7 +61,11 @@ export class Identity extends Domain {
|
||||
);
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<IdentityView> {
|
||||
decrypt(
|
||||
orgId: string,
|
||||
context: string = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<IdentityView> {
|
||||
return this.decryptObj(
|
||||
new IdentityView(),
|
||||
{
|
||||
@@ -86,6 +90,7 @@ export class Identity extends Domain {
|
||||
},
|
||||
orgId,
|
||||
encKey,
|
||||
"DomainType: Identity; " + context,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,11 @@ export class LoginUri extends Domain {
|
||||
);
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<LoginUriView> {
|
||||
decrypt(
|
||||
orgId: string,
|
||||
context: string = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<LoginUriView> {
|
||||
return this.decryptObj(
|
||||
new LoginUriView(this),
|
||||
{
|
||||
@@ -41,6 +45,7 @@ export class LoginUri extends Domain {
|
||||
},
|
||||
orgId,
|
||||
encKey,
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ export class Login extends Domain {
|
||||
async decrypt(
|
||||
orgId: string,
|
||||
bypassValidation: boolean,
|
||||
context: string = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<LoginView> {
|
||||
const view = await this.decryptObj(
|
||||
@@ -66,6 +67,7 @@ export class Login extends Domain {
|
||||
},
|
||||
orgId,
|
||||
encKey,
|
||||
`DomainType: Login; ${context}`,
|
||||
);
|
||||
|
||||
if (this.uris != null) {
|
||||
@@ -76,7 +78,7 @@ export class Login extends Domain {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uri = await this.uris[i].decrypt(orgId, encKey);
|
||||
const uri = await this.uris[i].decrypt(orgId, context, encKey);
|
||||
// URIs are shared remotely after decryption
|
||||
// we need to validate that the string hasn't been changed by a compromised server
|
||||
// This validation is tied to the existence of cypher.key for backwards compatibility
|
||||
|
||||
@@ -32,6 +32,7 @@ export class Password extends Domain {
|
||||
},
|
||||
orgId,
|
||||
encKey,
|
||||
"DomainType: PasswordHistory",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,12 @@ export class SecureNote extends Domain {
|
||||
this.type = obj.type;
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<SecureNoteView> {
|
||||
return Promise.resolve(new SecureNoteView(this));
|
||||
async decrypt(
|
||||
orgId: string,
|
||||
context = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<SecureNoteView> {
|
||||
return new SecureNoteView(this);
|
||||
}
|
||||
|
||||
toSecureNoteData(): SecureNoteData {
|
||||
|
||||
@@ -32,7 +32,11 @@ export class SshKey extends Domain {
|
||||
);
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<SshKeyView> {
|
||||
decrypt(
|
||||
orgId: string,
|
||||
context = "No Cipher Context",
|
||||
encKey?: SymmetricCryptoKey,
|
||||
): Promise<SshKeyView> {
|
||||
return this.decryptObj(
|
||||
new SshKeyView(),
|
||||
{
|
||||
@@ -42,6 +46,7 @@ export class SshKey extends Domain {
|
||||
},
|
||||
orgId,
|
||||
encKey,
|
||||
"DomainType: SshKey; " + context,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{html,ts}",
|
||||
"../../libs/components/src/**/*.{html,ts}",
|
||||
"../../libs/key-management/src/**/*.{html,ts}",
|
||||
"../../libs/auth/src/**/*.{html,ts}",
|
||||
],
|
||||
safelist: [],
|
||||
|
||||
@@ -70,6 +70,9 @@ const clientTypeToSuccessRouteRecord: Partial<Record<ClientType, string>> = {
|
||||
[ClientType.Browser]: "/tabs/current",
|
||||
};
|
||||
|
||||
/// The minimum amount of time to wait after a process reload for a biometrics auto prompt to be possible
|
||||
/// Fixes safari autoprompt behavior
|
||||
const AUTOPROMPT_BIOMETRICS_PROCESS_RELOAD_DELAY = 5000;
|
||||
@Component({
|
||||
selector: "bit-lock",
|
||||
templateUrl: "lock.component.html",
|
||||
@@ -304,7 +307,13 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||
(await this.biometricService.getShouldAutopromptNow())
|
||||
) {
|
||||
await this.biometricService.setShouldAutopromptNow(false);
|
||||
await this.unlockViaBiometrics();
|
||||
if (
|
||||
(await this.biometricStateService.getLastProcessReload()) == null ||
|
||||
Date.now() - (await this.biometricStateService.getLastProcessReload()).getTime() >
|
||||
AUTOPROMPT_BIOMETRICS_PROCESS_RELOAD_DELAY
|
||||
) {
|
||||
await this.unlockViaBiometrics();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
PROMPT_AUTOMATICALLY,
|
||||
PROMPT_CANCELLED,
|
||||
FINGERPRINT_VALIDATED,
|
||||
LAST_PROCESS_RELOAD,
|
||||
} from "./biometric.state";
|
||||
|
||||
export abstract class BiometricStateService {
|
||||
@@ -106,6 +107,10 @@ export abstract class BiometricStateService {
|
||||
*/
|
||||
abstract setFingerprintValidated(validated: boolean): Promise<void>;
|
||||
|
||||
abstract updateLastProcessReload(): Promise<void>;
|
||||
|
||||
abstract getLastProcessReload(): Promise<Date>;
|
||||
|
||||
abstract logout(userId: UserId): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -117,6 +122,7 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
private promptCancelledState: GlobalState<Record<UserId, boolean>>;
|
||||
private promptAutomaticallyState: ActiveUserState<boolean>;
|
||||
private fingerprintValidatedState: GlobalState<boolean>;
|
||||
private lastProcessReloadState: GlobalState<Date>;
|
||||
biometricUnlockEnabled$: Observable<boolean>;
|
||||
encryptedClientKeyHalf$: Observable<EncString | undefined>;
|
||||
requirePasswordOnStart$: Observable<boolean>;
|
||||
@@ -124,6 +130,7 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
promptCancelled$: Observable<boolean>;
|
||||
promptAutomatically$: Observable<boolean>;
|
||||
fingerprintValidated$: Observable<boolean>;
|
||||
lastProcessReload$: Observable<Date>;
|
||||
|
||||
constructor(private stateProvider: StateProvider) {
|
||||
this.biometricUnlockEnabledState = this.stateProvider.getActive(BIOMETRIC_UNLOCK_ENABLED);
|
||||
@@ -159,6 +166,9 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
|
||||
this.fingerprintValidatedState = this.stateProvider.getGlobal(FINGERPRINT_VALIDATED);
|
||||
this.fingerprintValidated$ = this.fingerprintValidatedState.state$.pipe(map(Boolean));
|
||||
|
||||
this.lastProcessReloadState = this.stateProvider.getGlobal(LAST_PROCESS_RELOAD);
|
||||
this.lastProcessReload$ = this.lastProcessReloadState.state$;
|
||||
}
|
||||
|
||||
async setBiometricUnlockEnabled(enabled: boolean): Promise<void> {
|
||||
@@ -270,6 +280,14 @@ export class DefaultBiometricStateService implements BiometricStateService {
|
||||
async setFingerprintValidated(validated: boolean): Promise<void> {
|
||||
await this.fingerprintValidatedState.update(() => validated);
|
||||
}
|
||||
|
||||
async updateLastProcessReload(): Promise<void> {
|
||||
await this.lastProcessReloadState.update(() => new Date());
|
||||
}
|
||||
|
||||
async getLastProcessReload(): Promise<Date> {
|
||||
return await firstValueFrom(this.lastProcessReload$);
|
||||
}
|
||||
}
|
||||
|
||||
function encryptedClientKeyHalfToEncString(
|
||||
|
||||
@@ -95,3 +95,14 @@ export const FINGERPRINT_VALIDATED = new KeyDefinition<boolean>(
|
||||
deserializer: (obj) => obj,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Last process reload time
|
||||
*/
|
||||
export const LAST_PROCESS_RELOAD = new KeyDefinition<Date>(
|
||||
BIOMETRIC_SETTINGS_DISK,
|
||||
"lastProcessReload",
|
||||
{
|
||||
deserializer: (obj) => new Date(obj),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -462,6 +462,7 @@ describe("keyService", () => {
|
||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(
|
||||
fakeEncryptedUserPrivateKey,
|
||||
userKey,
|
||||
"Content: Encrypted Private Key",
|
||||
);
|
||||
|
||||
expect(userPrivateKey).toBe(fakeDecryptedUserPrivateKey);
|
||||
|
||||
@@ -382,7 +382,6 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
key: org.key,
|
||||
};
|
||||
});
|
||||
|
||||
return encOrgKeyData;
|
||||
});
|
||||
}
|
||||
@@ -891,6 +890,7 @@ export class DefaultKeyService implements KeyServiceAbstraction {
|
||||
return (await this.encryptService.decryptToBytes(
|
||||
new EncString(encryptedPrivateKey),
|
||||
key,
|
||||
"Content: Encrypted Private Key",
|
||||
)) as UserPrivateKey;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,12 +4,8 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
// FIXME: use index.ts imports once policy abstractions and models
|
||||
// implement ADR-0002
|
||||
import { Policy } from "@bitwarden/common/admin-console/models/domain/policy";
|
||||
import {
|
||||
CredentialAlgorithm,
|
||||
EmailAlgorithms,
|
||||
PasswordAlgorithms,
|
||||
UsernameAlgorithms,
|
||||
} from "@bitwarden/generator-core";
|
||||
|
||||
import { CredentialAlgorithm, EmailAlgorithms, PasswordAlgorithms, UsernameAlgorithms } from "..";
|
||||
|
||||
/** Reduces policies to a set of available algorithms
|
||||
* @param policies the policies to reduce
|
||||
|
||||
Reference in New Issue
Block a user