mirror of
https://github.com/bitwarden/browser
synced 2026-02-04 18:53:20 +00:00
Merge branch 'main' into km/beeep/clean-agent-rewrite
This commit is contained in:
@@ -886,6 +886,7 @@ export default class MainBackground {
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
this.authRequestApiService,
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
|
||||
@@ -1673,7 +1674,6 @@ export default class MainBackground {
|
||||
await Promise.all([
|
||||
this.keyService.clearKeys(userBeingLoggedOut),
|
||||
this.cipherService.clear(userBeingLoggedOut),
|
||||
// ! DO NOT REMOVE folderService.clear ! For more information see PM-25660
|
||||
this.folderService.clear(userBeingLoggedOut),
|
||||
this.vaultTimeoutSettingsService.clear(userBeingLoggedOut),
|
||||
this.biometricStateService.logout(userBeingLoggedOut),
|
||||
|
||||
@@ -621,7 +621,7 @@ export class LoginCommand {
|
||||
const newPasswordHash = await this.keyService.hashMasterKey(masterPassword, newMasterKey);
|
||||
|
||||
// Grab user key
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
if (!userKey) {
|
||||
throw new Error("User key not found.");
|
||||
}
|
||||
|
||||
@@ -657,6 +657,7 @@ export class ServiceContainer {
|
||||
this.apiService,
|
||||
this.stateProvider,
|
||||
this.authRequestApiService,
|
||||
this.accountService,
|
||||
);
|
||||
|
||||
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
|
||||
@@ -948,7 +949,6 @@ export class ServiceContainer {
|
||||
this.eventUploadService.uploadEvents(userId as UserId),
|
||||
this.keyService.clearKeys(userId),
|
||||
this.cipherService.clear(userId),
|
||||
// ! DO NOT REMOVE folderService.clear ! For more information see PM-25660
|
||||
this.folderService.clear(userId),
|
||||
]);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ use anyhow::Result;
|
||||
#[cfg(target_env = "gnu")]
|
||||
use libc::c_uint;
|
||||
use libc::{self, c_int};
|
||||
use tracing::info;
|
||||
|
||||
// RLIMIT_CORE is the maximum size of a core dump file. Setting both to 0 disables core dumps, on crashes
|
||||
// https://github.com/torvalds/linux/blob/1613e604df0cd359cf2a7fbd9be7a0bcfacfabd0/include/uapi/asm-generic/resource.h#L20
|
||||
@@ -20,7 +21,7 @@ pub fn disable_coredumps() -> Result<()> {
|
||||
rlim_cur: 0,
|
||||
rlim_max: 0,
|
||||
};
|
||||
println!("[Process Isolation] Disabling core dumps via setrlimit");
|
||||
info!("Disabling core dumps via setrlimit.");
|
||||
|
||||
if unsafe { libc::setrlimit(RLIMIT_CORE, &rlimit) } != 0 {
|
||||
let e = std::io::Error::last_os_error();
|
||||
@@ -48,9 +49,9 @@ pub fn is_core_dumping_disabled() -> Result<bool> {
|
||||
|
||||
pub fn isolate_process() -> Result<()> {
|
||||
let pid = std::process::id();
|
||||
println!(
|
||||
"[Process Isolation] Disabling ptrace and memory access for main ({}) via PR_SET_DUMPABLE",
|
||||
pid
|
||||
info!(
|
||||
pid,
|
||||
"Disabling ptrace and memory access for main via PR_SET_DUMPABLE."
|
||||
);
|
||||
|
||||
if unsafe { libc::prctl(PR_SET_DUMPABLE, 0) } != 0 {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::{bail, Result};
|
||||
use tracing::info;
|
||||
|
||||
pub fn disable_coredumps() -> Result<()> {
|
||||
bail!("Not implemented on Mac")
|
||||
@@ -10,10 +11,7 @@ pub fn is_core_dumping_disabled() -> Result<bool> {
|
||||
|
||||
pub fn isolate_process() -> Result<()> {
|
||||
let pid: u32 = std::process::id();
|
||||
println!(
|
||||
"[Process Isolation] Disabling ptrace on main process ({}) via PT_DENY_ATTACH",
|
||||
pid
|
||||
);
|
||||
info!(pid, "Disabling ptrace on main process via PT_DENY_ATTACH.");
|
||||
|
||||
secmem_proc::harden_process().map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use anyhow::{bail, Result};
|
||||
use tracing::info;
|
||||
|
||||
pub fn disable_coredumps() -> Result<()> {
|
||||
bail!("Not implemented on Windows")
|
||||
@@ -10,10 +11,7 @@ pub fn is_core_dumping_disabled() -> Result<bool> {
|
||||
|
||||
pub fn isolate_process() -> Result<()> {
|
||||
let pid: u32 = std::process::id();
|
||||
println!(
|
||||
"[Process Isolation] Isolating main process via DACL {}",
|
||||
pid
|
||||
);
|
||||
info!(pid, "Isolating main process via DACL.");
|
||||
|
||||
secmem_proc::harden_process().map_err(|e| {
|
||||
anyhow::anyhow!(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.10.1",
|
||||
"version": "2025.10.2",
|
||||
"keywords": [
|
||||
"bitwarden",
|
||||
"password",
|
||||
|
||||
@@ -691,7 +691,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
|
||||
await this.keyService.clearKeys(userBeingLoggedOut);
|
||||
await this.cipherService.clear(userBeingLoggedOut);
|
||||
// ! DO NOT REMOVE folderService.clear ! For more information see PM-25660
|
||||
await this.folderService.clear(userBeingLoggedOut);
|
||||
await this.vaultTimeoutSettingsService.clear(userBeingLoggedOut);
|
||||
await this.biometricStateService.logout(userBeingLoggedOut);
|
||||
|
||||
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.10.1",
|
||||
"version": "2025.10.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.10.1",
|
||||
"version": "2025.10.2",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@bitwarden/desktop-napi": "file:../desktop_native/napi"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "@bitwarden/desktop",
|
||||
"productName": "Bitwarden",
|
||||
"description": "A secure and free password manager for all of your devices.",
|
||||
"version": "2025.10.1",
|
||||
"version": "2025.10.2",
|
||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||
"homepage": "https://bitwarden.com",
|
||||
"license": "GPL-3.0",
|
||||
|
||||
@@ -258,7 +258,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
await Promise.all([
|
||||
this.keyService.clearKeys(userId),
|
||||
this.cipherService.clear(userId),
|
||||
// ! DO NOT REMOVE folderService.clear ! For more information see PM-25660
|
||||
this.folderService.clear(userId),
|
||||
this.biometricStateService.logout(userId),
|
||||
]);
|
||||
|
||||
@@ -17,6 +17,7 @@ import { CsprngArray } from "@bitwarden/common/types/csprng";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey, MasterKey, UserPrivateKey } from "@bitwarden/common/types/key";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import { Argon2KdfConfig, KdfType, KeyService, PBKDF2KdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { EmergencyAccessStatusType } from "../enums/emergency-access-status-type";
|
||||
@@ -44,6 +45,7 @@ describe("EmergencyAccessService", () => {
|
||||
|
||||
const mockNewUserKey = new SymmetricCryptoKey(new Uint8Array(64)) as UserKey;
|
||||
const mockTrustedPublicKeys = [Utils.fromUtf8ToArray("trustedPublicKey")];
|
||||
const mockUserId = newGuid() as UserId;
|
||||
|
||||
beforeAll(() => {
|
||||
emergencyAccessApiService = mock<EmergencyAccessApiService>();
|
||||
@@ -125,7 +127,7 @@ describe("EmergencyAccessService", () => {
|
||||
"mockUserPublicKeyEncryptedUserKey",
|
||||
);
|
||||
|
||||
keyService.getUserKey.mockResolvedValueOnce(mockUserKey);
|
||||
keyService.userKey$.mockReturnValue(of(mockUserKey));
|
||||
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValueOnce(
|
||||
mockUserPublicKeyEncryptedUserKey,
|
||||
@@ -134,7 +136,7 @@ describe("EmergencyAccessService", () => {
|
||||
emergencyAccessApiService.postEmergencyAccessConfirm.mockResolvedValueOnce();
|
||||
|
||||
// Act
|
||||
await emergencyAccessService.confirm(id, granteeId, publicKey);
|
||||
await emergencyAccessService.confirm(id, granteeId, publicKey, mockUserId);
|
||||
|
||||
// Assert
|
||||
expect(emergencyAccessApiService.postEmergencyAccessConfirm).toHaveBeenCalledWith(id, {
|
||||
|
||||
@@ -175,11 +175,17 @@ export class EmergencyAccessService
|
||||
* Step 3 of the 3 step setup flow.
|
||||
* Intended for grantor.
|
||||
* @param id emergency access id
|
||||
* @param token secret token provided in email
|
||||
* @param granteeId id of the grantee
|
||||
* @param publicKey public key of grantee
|
||||
* @param activeUserId the active user's id
|
||||
*/
|
||||
async confirm(id: string, granteeId: string, publicKey: Uint8Array): Promise<void> {
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
async confirm(
|
||||
id: string,
|
||||
granteeId: string,
|
||||
publicKey: Uint8Array,
|
||||
activeUserId: UserId,
|
||||
): Promise<void> {
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(activeUserId));
|
||||
if (!userKey) {
|
||||
throw new Error("No user key found");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { lastValueFrom, Observable, firstValueFrom, switchMap } from "rxjs";
|
||||
import { lastValueFrom, Observable, firstValueFrom, switchMap, map } from "rxjs";
|
||||
|
||||
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
|
||||
import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe";
|
||||
@@ -165,7 +165,15 @@ export class EmergencyAccessComponent implements OnInit {
|
||||
});
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
if (result === EmergencyAccessConfirmDialogResult.Confirmed) {
|
||||
await this.emergencyAccessService.confirm(contact.id, contact.granteeId, publicKey);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(getUserId),
|
||||
);
|
||||
await this.emergencyAccessService.confirm(
|
||||
contact.id,
|
||||
contact.granteeId,
|
||||
publicKey,
|
||||
activeUserId,
|
||||
);
|
||||
updateUser();
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
@@ -176,10 +184,14 @@ export class EmergencyAccessComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
this.actionPromise = this.emergencyAccessService.confirm(
|
||||
contact.id,
|
||||
contact.granteeId,
|
||||
publicKey,
|
||||
activeUserId,
|
||||
);
|
||||
await this.actionPromise;
|
||||
updateUser();
|
||||
|
||||
@@ -31,11 +31,19 @@
|
||||
</div>
|
||||
}
|
||||
<div class="tw-pb-8 !tw-mx-0">
|
||||
<h5 bitTypography="h5">{{ "paymentMethod" | i18n }}</h5>
|
||||
<app-enter-payment-method
|
||||
[showBankAccount]="isFamiliesPlan"
|
||||
[group]="formGroup.controls.paymentForm"
|
||||
[includeBillingAddress]="true"
|
||||
[includeBillingAddress]="false"
|
||||
#paymentComponent
|
||||
></app-enter-payment-method>
|
||||
<h5 bitTypography="h5" class="tw-pt-4 tw-pb-2">{{ "billingAddress" | i18n }}</h5>
|
||||
<app-enter-billing-address
|
||||
[group]="formGroup.controls.billingAddress"
|
||||
[scenario]="{ type: 'checkout', supportsTaxId: false }"
|
||||
>
|
||||
</app-enter-billing-address>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { debounceTime, Observable } from "rxjs";
|
||||
import { catchError, debounceTime, from, Observable, of, switchMap } from "rxjs";
|
||||
|
||||
import { Account } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
@@ -20,7 +20,10 @@ import { LogService } from "@bitwarden/logging";
|
||||
import { CartSummaryComponent, LineItem } from "@bitwarden/pricing";
|
||||
import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { EnterPaymentMethodComponent } from "../../../payment/components";
|
||||
import {
|
||||
EnterBillingAddressComponent,
|
||||
EnterPaymentMethodComponent,
|
||||
} from "../../../payment/components";
|
||||
import { BillingServicesModule } from "../../../services";
|
||||
import { SubscriptionPricingService } from "../../../services/subscription-pricing.service";
|
||||
import { BitwardenSubscriber } from "../../../types";
|
||||
@@ -65,6 +68,7 @@ export type UpgradePaymentParams = {
|
||||
CartSummaryComponent,
|
||||
ButtonModule,
|
||||
EnterPaymentMethodComponent,
|
||||
EnterBillingAddressComponent,
|
||||
BillingServicesModule,
|
||||
],
|
||||
providers: [UpgradePaymentService],
|
||||
@@ -83,6 +87,7 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
|
||||
protected formGroup = new FormGroup({
|
||||
organizationName: new FormControl<string>("", [Validators.required]),
|
||||
paymentForm: EnterPaymentMethodComponent.getFormGroup(),
|
||||
billingAddress: EnterBillingAddressComponent.getFormGroup(),
|
||||
});
|
||||
|
||||
protected loading = signal(true);
|
||||
@@ -140,9 +145,16 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
});
|
||||
|
||||
this.formGroup.valueChanges
|
||||
.pipe(debounceTime(1000), takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(() => this.refreshSalesTax());
|
||||
this.formGroup.controls.billingAddress.valueChanges
|
||||
.pipe(
|
||||
debounceTime(1000),
|
||||
// Only proceed when form has required values
|
||||
switchMap(() => this.refreshSalesTax$()),
|
||||
takeUntilDestroyed(this.destroyRef),
|
||||
)
|
||||
.subscribe((tax) => {
|
||||
this.estimatedTax = tax;
|
||||
});
|
||||
this.loading.set(false);
|
||||
}
|
||||
|
||||
@@ -199,8 +211,8 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
|
||||
|
||||
private async processUpgrade(): Promise<UpgradePaymentResult> {
|
||||
// Get common values
|
||||
const country = this.formGroup.value?.paymentForm?.billingAddress?.country;
|
||||
const postalCode = this.formGroup.value?.paymentForm?.billingAddress?.postalCode;
|
||||
const country = this.formGroup.value?.billingAddress?.country;
|
||||
const postalCode = this.formGroup.value?.billingAddress?.postalCode;
|
||||
|
||||
if (!this.selectedPlan) {
|
||||
throw new Error("No plan selected");
|
||||
@@ -246,19 +258,20 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshSalesTax(): Promise<void> {
|
||||
// Create an observable for tax calculation
|
||||
private refreshSalesTax$(): Observable<number> {
|
||||
const billingAddress = {
|
||||
country: this.formGroup.value.paymentForm?.billingAddress?.country,
|
||||
postalCode: this.formGroup.value.paymentForm?.billingAddress?.postalCode,
|
||||
country: this.formGroup.value?.billingAddress?.country,
|
||||
postalCode: this.formGroup.value?.billingAddress?.postalCode,
|
||||
};
|
||||
|
||||
if (!this.selectedPlan || !billingAddress.country || !billingAddress.postalCode) {
|
||||
this.estimatedTax = 0;
|
||||
return;
|
||||
return of(0);
|
||||
}
|
||||
|
||||
this.upgradePaymentService
|
||||
.calculateEstimatedTax(this.selectedPlan, {
|
||||
// Convert Promise to Observable
|
||||
return from(
|
||||
this.upgradePaymentService.calculateEstimatedTax(this.selectedPlan, {
|
||||
line1: null,
|
||||
line2: null,
|
||||
city: null,
|
||||
@@ -266,17 +279,16 @@ export class UpgradePaymentComponent implements OnInit, AfterViewInit {
|
||||
country: billingAddress.country,
|
||||
postalCode: billingAddress.postalCode,
|
||||
taxId: null,
|
||||
})
|
||||
.then((tax) => {
|
||||
this.estimatedTax = tax;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
}),
|
||||
).pipe(
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error("Tax calculation failed:", error);
|
||||
this.toastService.showToast({
|
||||
variant: "error",
|
||||
message: this.i18nService.t("taxCalculationError"),
|
||||
});
|
||||
this.estimatedTax = 0;
|
||||
});
|
||||
return of(0); // Return default value on error
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export class SetPinComponent implements OnInit {
|
||||
}
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
const userKey = await this.keyService.getUserKey();
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
|
||||
const userKeyEncryptedPin = await this.pinService.createUserKeyEncryptedPin(
|
||||
pinFormControl.value,
|
||||
|
||||
@@ -1291,6 +1291,7 @@ const safeProviders: SafeProvider[] = [
|
||||
ApiServiceAbstraction,
|
||||
StateProvider,
|
||||
AuthRequestApiServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
@@ -55,6 +55,7 @@ export abstract class AuthRequestServiceAbstraction {
|
||||
* Approve or deny an auth request.
|
||||
* @param approve True to approve, false to deny.
|
||||
* @param authRequest The auth request to approve or deny, must have an id and key.
|
||||
* @param activeUserId the active user id
|
||||
* @returns The updated auth request, the `requestApproved` field will be true if
|
||||
* approval was successful.
|
||||
* @throws If the auth request is missing an id or key.
|
||||
|
||||
@@ -337,7 +337,7 @@ describe("LoginStrategy", () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.privateKey = null;
|
||||
keyService.makeKeyPair.mockResolvedValue(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
|
||||
keyService.getUserKey.mockResolvedValue(userKey);
|
||||
keyService.userKey$.mockReturnValue(new BehaviorSubject<UserKey>(userKey).asObservable());
|
||||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
@@ -356,9 +356,11 @@ describe("LoginStrategy", () => {
|
||||
});
|
||||
|
||||
it("throws if userKey is CoseEncrypt0 (V2 encryption) in createKeyPairForOldAccount", async () => {
|
||||
keyService.getUserKey.mockResolvedValue({
|
||||
inner: () => ({ type: 7 }),
|
||||
} as UserKey);
|
||||
keyService.userKey$.mockReturnValue(
|
||||
new BehaviorSubject<UserKey>({
|
||||
inner: () => ({ type: 7 }),
|
||||
} as unknown as UserKey).asObservable(),
|
||||
);
|
||||
await expect(passwordLoginStrategy["createKeyPairForOldAccount"](userId)).resolves.toBe(
|
||||
undefined,
|
||||
);
|
||||
|
||||
@@ -306,7 +306,11 @@ export abstract class LoginStrategy {
|
||||
|
||||
protected async createKeyPairForOldAccount(userId: UserId) {
|
||||
try {
|
||||
const userKey = await this.keyService.getUserKey(userId);
|
||||
const userKey = await firstValueFrom(this.keyService.userKey$(userId));
|
||||
if (userKey === null) {
|
||||
throw new Error("User key is null when creating key pair for old account");
|
||||
}
|
||||
|
||||
if (userKey.inner().type == EncryptionType.CoseEncrypt0) {
|
||||
throw new Error("Cannot create key pair for account on V2 encryption");
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
@@ -9,11 +10,11 @@ import { FakeMasterPasswordService } from "@bitwarden/common/key-management/mast
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||
import { StateProvider } from "@bitwarden/common/platform/state";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
||||
import { newGuid } from "@bitwarden/guid";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { DefaultAuthRequestApiService } from "./auth-request-api.service";
|
||||
@@ -29,10 +30,11 @@ describe("AuthRequestService", () => {
|
||||
const encryptService = mock<EncryptService>();
|
||||
const apiService = mock<ApiService>();
|
||||
const authRequestApiService = mock<DefaultAuthRequestApiService>();
|
||||
const accountService = mock<AccountService>();
|
||||
|
||||
let mockPrivateKey: Uint8Array;
|
||||
let mockPublicKey: Uint8Array;
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const mockUserId = newGuid() as UserId;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -46,6 +48,7 @@ describe("AuthRequestService", () => {
|
||||
apiService,
|
||||
stateProvider,
|
||||
authRequestApiService,
|
||||
accountService,
|
||||
);
|
||||
|
||||
mockPrivateKey = new Uint8Array(64);
|
||||
@@ -95,6 +98,8 @@ describe("AuthRequestService", () => {
|
||||
const authRequestNoId = new AuthRequestResponse({ id: "", key: "KEY" });
|
||||
const authRequestNoPublicKey = new AuthRequestResponse({ id: "123", publicKey: "" });
|
||||
|
||||
accountService.activeAccount$ = of({ id: mockUserId } as any);
|
||||
|
||||
await expect(sut.approveOrDenyAuthRequest(true, authRequestNoId)).rejects.toThrow(
|
||||
"Auth request has no id",
|
||||
);
|
||||
@@ -104,8 +109,9 @@ describe("AuthRequestService", () => {
|
||||
});
|
||||
|
||||
it("should use the user key if the master key and hash do not exist", async () => {
|
||||
keyService.getUserKey.mockResolvedValueOnce(
|
||||
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
|
||||
accountService.activeAccount$ = of({ id: mockUserId } as any);
|
||||
keyService.userKey$.mockReturnValue(
|
||||
of(new SymmetricCryptoKey(new Uint8Array(64)) as UserKey),
|
||||
);
|
||||
|
||||
await sut.approveOrDenyAuthRequest(
|
||||
|
||||
@@ -4,9 +4,11 @@ import { Observable, Subject, defer, firstValueFrom, map } from "rxjs";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { EncString } from "@bitwarden/common/key-management/crypto/models/enc-string";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
@@ -56,6 +58,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
private apiService: ApiService,
|
||||
private stateProvider: StateProvider,
|
||||
private authRequestApiService: AuthRequestApiServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
) {
|
||||
this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable();
|
||||
this.adminLoginApproved$ = this.adminLoginApprovedSubject.asObservable();
|
||||
@@ -124,15 +127,19 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||
approve: boolean,
|
||||
authRequest: AuthRequestResponse,
|
||||
): Promise<AuthRequestResponse> {
|
||||
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
|
||||
|
||||
if (!authRequest.id) {
|
||||
throw new Error("Auth request has no id");
|
||||
}
|
||||
if (!authRequest.publicKey) {
|
||||
throw new Error("Auth request has no public key");
|
||||
}
|
||||
if (activeUserId == null) {
|
||||
throw new Error("User ID is required");
|
||||
}
|
||||
const pubKey = Utils.fromB64ToArray(authRequest.publicKey);
|
||||
|
||||
const keyToEncrypt = await this.keyService.getUserKey();
|
||||
const keyToEncrypt = await firstValueFrom(this.keyService.userKey$(activeUserId));
|
||||
const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(keyToEncrypt, pubKey);
|
||||
|
||||
const response = new PasswordlessAuthRequest(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BehaviorSubject, of } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -103,7 +103,7 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||
};
|
||||
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
|
||||
|
||||
keyService.getUserKey.mockResolvedValue({ key: "key" } as any);
|
||||
keyService.userKey$.mockReturnValue(of({ key: "key" } as any));
|
||||
encryptService.encapsulateKeyUnsigned.mockResolvedValue(encryptedKey as any);
|
||||
|
||||
await service.enroll("orgId");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -8,9 +8,11 @@ import {
|
||||
OrganizationUserApiService,
|
||||
OrganizationUserResetPasswordEnrollmentRequest,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { UserId } from "@bitwarden/user-core";
|
||||
|
||||
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
|
||||
@@ -43,7 +45,7 @@ export class PasswordResetEnrollmentServiceImplementation
|
||||
|
||||
async enroll(organizationId: string): Promise<void>;
|
||||
async enroll(organizationId: string, userId: string, userKey: UserKey): Promise<void>;
|
||||
async enroll(organizationId: string, userId?: string, userKey?: UserKey): Promise<void> {
|
||||
async enroll(organizationId: string, activeUserId?: string, userKey?: UserKey): Promise<void> {
|
||||
const orgKeyResponse = await this.organizationApiService.getKeys(organizationId);
|
||||
if (orgKeyResponse == null) {
|
||||
throw new Error(this.i18nService.t("resetPasswordOrgKeysError"));
|
||||
@@ -51,9 +53,12 @@ export class PasswordResetEnrollmentServiceImplementation
|
||||
|
||||
const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey);
|
||||
|
||||
userId =
|
||||
userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
|
||||
userKey = userKey ?? (await this.keyService.getUserKey(userId));
|
||||
activeUserId =
|
||||
activeUserId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)));
|
||||
if (activeUserId == null) {
|
||||
throw new Error("User ID is required");
|
||||
}
|
||||
userKey = userKey ?? (await firstValueFrom(this.keyService.userKey$(activeUserId as UserId)));
|
||||
// RSA Encrypt user's userKey.key with organization public key
|
||||
const encryptedKey = await this.encryptService.encapsulateKeyUnsigned(userKey, orgPublicKey);
|
||||
|
||||
@@ -62,7 +67,7 @@ export class PasswordResetEnrollmentServiceImplementation
|
||||
|
||||
await this.organizationUserApiService.putOrganizationUserResetPasswordEnrollment(
|
||||
organizationId,
|
||||
userId,
|
||||
activeUserId,
|
||||
resetRequest,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,7 +144,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
|
||||
|
||||
await this.searchService.clearIndex(lockingUserId);
|
||||
|
||||
// ! DO NOT REMOVE folderService.clearDecryptedFolderState ! For more information see PM-25660
|
||||
await this.folderService.clearDecryptedFolderState(lockingUserId);
|
||||
await this.masterPasswordService.clearMasterKey(lockingUserId);
|
||||
|
||||
|
||||
@@ -31,8 +31,6 @@ export class DefaultSingleUserState<T>
|
||||
|
||||
protected override async doStorageSave(newState: T, oldState: T): Promise<void> {
|
||||
await super.doStorageSave(newState, oldState);
|
||||
if (newState != null && oldState == null) {
|
||||
await this.stateEventRegistrarService.registerEvents(this.keyDefinition);
|
||||
}
|
||||
await this.stateEventRegistrarService.registerEvents(this.keyDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -279,7 +279,7 @@
|
||||
},
|
||||
"apps/desktop": {
|
||||
"name": "@bitwarden/desktop",
|
||||
"version": "2025.10.1",
|
||||
"version": "2025.10.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "GPL-3.0"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user