Merge branch 'main' into auth/pm-7392/token-service-add-secure-storage-fallback
This commit is contained in:
commit
457e7a4819
|
@ -1,5 +1,3 @@
|
|||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
@ -15,21 +13,13 @@ import { MigrationRunner } from "@bitwarden/common/platform/services/migration-r
|
|||
import { StateService as BaseStateService } from "@bitwarden/common/platform/services/state.service";
|
||||
|
||||
import { Account } from "../../models/account";
|
||||
import { browserSession, sessionSync } from "../decorators/session-sync-observable";
|
||||
|
||||
import { BrowserStateService } from "./abstractions/browser-state.service";
|
||||
|
||||
@browserSession
|
||||
export class DefaultBrowserStateService
|
||||
extends BaseStateService<GlobalState, Account>
|
||||
implements BrowserStateService
|
||||
{
|
||||
@sessionSync({
|
||||
initializer: Account.fromJSON as any, // TODO: Remove this any when all any types are removed from Account
|
||||
initializeAs: "record",
|
||||
})
|
||||
protected accountsSubject: BehaviorSubject<{ [userId: string]: Account }>;
|
||||
|
||||
protected accountDeserializer = Account.fromJSON;
|
||||
|
||||
constructor(
|
||||
|
|
|
@ -219,8 +219,10 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
await this.vaultTimeoutService.lock(message.userId);
|
||||
break;
|
||||
case "lockAllVaults": {
|
||||
const currentUser = await this.stateService.getUserId();
|
||||
const accounts = await firstValueFrom(this.stateService.accounts$);
|
||||
const currentUser = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a.id)),
|
||||
);
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
await this.vaultTimeoutService.lock(currentUser);
|
||||
for (const account of Object.keys(accounts)) {
|
||||
if (account === currentUser) {
|
||||
|
@ -701,7 +703,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private async checkForSystemTimeout(timeout: number): Promise<void> {
|
||||
const accounts = await firstValueFrom(this.stateService.accounts$);
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
for (const userId in accounts) {
|
||||
if (userId == null) {
|
||||
continue;
|
||||
|
|
|
@ -221,7 +221,7 @@ const safeProviders: SafeProvider[] = [
|
|||
safeProvider({
|
||||
provide: EncryptedMessageHandlerService,
|
||||
deps: [
|
||||
StateServiceAbstraction,
|
||||
AccountServiceAbstraction,
|
||||
AuthServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
PolicyServiceAbstraction,
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||
import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
@ -28,7 +29,7 @@ import { UserStatusErrorResponse } from "../models/native-messaging/encrypted-me
|
|||
|
||||
export class EncryptedMessageHandlerService {
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private accountService: AccountService,
|
||||
private authService: AuthService,
|
||||
private cipherService: CipherService,
|
||||
private policyService: PolicyService,
|
||||
|
@ -62,7 +63,9 @@ export class EncryptedMessageHandlerService {
|
|||
}
|
||||
|
||||
private async checkUserStatus(userId: string): Promise<string> {
|
||||
const activeUserId = await this.stateService.getUserId();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
if (userId !== activeUserId) {
|
||||
return "not-active-user";
|
||||
|
@ -77,17 +80,19 @@ export class EncryptedMessageHandlerService {
|
|||
}
|
||||
|
||||
private async statusCommandHandler(): Promise<AccountStatusResponse[]> {
|
||||
const accounts = await firstValueFrom(this.stateService.accounts$);
|
||||
const activeUserId = await this.stateService.getUserId();
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
|
||||
if (!accounts || !Object.keys(accounts)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
Object.keys(accounts).map(async (userId) => {
|
||||
Object.keys(accounts).map(async (userId: UserId) => {
|
||||
const authStatus = await this.authService.getAuthStatus(userId);
|
||||
const email = await this.stateService.getEmail({ userId });
|
||||
const email = accounts[userId].email;
|
||||
|
||||
return {
|
||||
id: userId,
|
||||
|
@ -107,7 +112,9 @@ export class EncryptedMessageHandlerService {
|
|||
}
|
||||
|
||||
const ciphersResponse: CipherResponse[] = [];
|
||||
const activeUserId = await this.stateService.getUserId();
|
||||
const activeUserId = await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
|
||||
);
|
||||
const authStatus = await this.authService.getAuthStatus(activeUserId);
|
||||
|
||||
if (authStatus !== AuthenticationStatus.Unlocked) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Injectable, NgZone } from "@angular/core";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
@ -41,6 +42,7 @@ export class NativeMessagingService {
|
|||
private biometricStateService: BiometricStateService,
|
||||
private nativeMessageHandler: NativeMessageHandlerService,
|
||||
private dialogService: DialogService,
|
||||
private accountService: AccountService,
|
||||
private ngZone: NgZone,
|
||||
) {}
|
||||
|
||||
|
@ -51,9 +53,7 @@ export class NativeMessagingService {
|
|||
private async messageHandler(msg: LegacyMessageWrapper | Message) {
|
||||
const outerMessage = msg as Message;
|
||||
if (outerMessage.version) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.nativeMessageHandler.handleMessage(outerMessage);
|
||||
await this.nativeMessageHandler.handleMessage(outerMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ export class NativeMessagingService {
|
|||
const remotePublicKey = Utils.fromB64ToArray(rawMessage.publicKey);
|
||||
|
||||
// Validate the UserId to ensure we are logged into the same account.
|
||||
const accounts = await firstValueFrom(this.stateService.accounts$);
|
||||
const accounts = await firstValueFrom(this.accountService.accounts$);
|
||||
const userIds = Object.keys(accounts);
|
||||
if (!userIds.includes(rawMessage.userId)) {
|
||||
ipc.platform.nativeMessaging.sendMessage({
|
||||
|
@ -81,7 +81,7 @@ export class NativeMessagingService {
|
|||
});
|
||||
|
||||
const fingerprint = await this.cryptoService.getFingerprint(
|
||||
await this.stateService.getUserId(),
|
||||
rawMessage.userId,
|
||||
remotePublicKey,
|
||||
);
|
||||
|
||||
|
@ -98,9 +98,7 @@ export class NativeMessagingService {
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.secureCommunication(remotePublicKey, appId);
|
||||
await this.secureCommunication(remotePublicKey, appId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -144,9 +142,7 @@ export class NativeMessagingService {
|
|||
? firstValueFrom(this.biometricStateService.biometricUnlockEnabled$)
|
||||
: this.biometricStateService.getBiometricUnlockEnabled(message.userId as UserId);
|
||||
if (!(await biometricUnlockPromise)) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.send({ command: "biometricUnlock", response: "not enabled" }, appId);
|
||||
await this.send({ command: "biometricUnlock", response: "not enabled" }, appId);
|
||||
|
||||
return this.ngZone.run(() =>
|
||||
this.dialogService.openSimpleDialog({
|
||||
|
@ -172,9 +168,7 @@ export class NativeMessagingService {
|
|||
// we send the master key still for backwards compatibility
|
||||
// with older browser extensions
|
||||
// TODO: Remove after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3472)
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.send(
|
||||
await this.send(
|
||||
{
|
||||
command: "biometricUnlock",
|
||||
response: "unlocked",
|
||||
|
@ -184,14 +178,10 @@ export class NativeMessagingService {
|
|||
appId,
|
||||
);
|
||||
} else {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||
}
|
||||
} catch (e) {
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||
await this.send({ command: "biometricUnlock", response: "canceled" }, appId);
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
|
@ -153,6 +153,7 @@ describe("EmergencyAccessService", () => {
|
|||
} as EmergencyAccessTakeoverResponse);
|
||||
|
||||
const mockDecryptedGrantorUserKey = new Uint8Array(64);
|
||||
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64));
|
||||
cryptoService.rsaDecrypt.mockResolvedValueOnce(mockDecryptedGrantorUserKey);
|
||||
|
||||
const mockMasterKey = new SymmetricCryptoKey(new Uint8Array(64) as CsprngArray) as MasterKey;
|
||||
|
@ -197,6 +198,7 @@ describe("EmergencyAccessService", () => {
|
|||
kdf: KdfType.PBKDF2_SHA256,
|
||||
kdfIterations: 500,
|
||||
} as EmergencyAccessTakeoverResponse);
|
||||
cryptoService.getPrivateKey.mockResolvedValue(new Uint8Array(64));
|
||||
|
||||
await expect(
|
||||
emergencyAccessService.takeover(mockId, mockEmail, mockName),
|
||||
|
@ -204,6 +206,21 @@ describe("EmergencyAccessService", () => {
|
|||
|
||||
expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should throw an error if the users private key cannot be retrieved", async () => {
|
||||
emergencyAccessApiService.postEmergencyAccessTakeover.mockResolvedValueOnce({
|
||||
keyEncrypted: "EncryptedKey",
|
||||
kdf: KdfType.PBKDF2_SHA256,
|
||||
kdfIterations: 500,
|
||||
} as EmergencyAccessTakeoverResponse);
|
||||
cryptoService.getPrivateKey.mockResolvedValue(null);
|
||||
|
||||
await expect(emergencyAccessService.takeover(mockId, mockEmail, mockName)).rejects.toThrow(
|
||||
"user does not have a private key",
|
||||
);
|
||||
|
||||
expect(emergencyAccessApiService.postEmergencyAccessPassword).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getRotatedKeys", () => {
|
||||
|
|
|
@ -209,7 +209,16 @@ export class EmergencyAccessService {
|
|||
async getViewOnlyCiphers(id: string): Promise<CipherView[]> {
|
||||
const response = await this.emergencyAccessApiService.postEmergencyAccessView(id);
|
||||
|
||||
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(response.keyEncrypted);
|
||||
const activeUserPrivateKey = await this.cryptoService.getPrivateKey();
|
||||
|
||||
if (activeUserPrivateKey == null) {
|
||||
throw new Error("Active user does not have a private key, cannot get view only ciphers.");
|
||||
}
|
||||
|
||||
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(
|
||||
response.keyEncrypted,
|
||||
activeUserPrivateKey,
|
||||
);
|
||||
const grantorUserKey = new SymmetricCryptoKey(grantorKeyBuffer) as UserKey;
|
||||
|
||||
const ciphers = await this.encryptService.decryptItems(
|
||||
|
@ -229,7 +238,16 @@ export class EmergencyAccessService {
|
|||
async takeover(id: string, masterPassword: string, email: string) {
|
||||
const takeoverResponse = await this.emergencyAccessApiService.postEmergencyAccessTakeover(id);
|
||||
|
||||
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(takeoverResponse.keyEncrypted);
|
||||
const activeUserPrivateKey = await this.cryptoService.getPrivateKey();
|
||||
|
||||
if (activeUserPrivateKey == null) {
|
||||
throw new Error("Active user does not have a private key, cannot complete a takeover.");
|
||||
}
|
||||
|
||||
const grantorKeyBuffer = await this.cryptoService.rsaDecrypt(
|
||||
takeoverResponse.keyEncrypted,
|
||||
activeUserPrivateKey,
|
||||
);
|
||||
if (grantorKeyBuffer == null) {
|
||||
throw new Error("Failed to decrypt grantor key");
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { combineLatest, firstValueFrom, from } from "rxjs";
|
||||
import { concatMap, switchMap, takeUntil } from "rxjs/operators";
|
||||
import { firstValueFrom, from, map } from "rxjs";
|
||||
import { switchMap, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { canAccessBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction";
|
||||
import { PlanType } from "@bitwarden/common/billing/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
|
@ -40,10 +40,6 @@ export class ClientsComponent extends BaseClientsComponent {
|
|||
manageOrganizations = false;
|
||||
showAddExisting = false;
|
||||
|
||||
protected consolidatedBillingEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.EnableConsolidatedBilling,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private providerService: ProviderService,
|
||||
|
@ -75,15 +71,10 @@ export class ClientsComponent extends BaseClientsComponent {
|
|||
.pipe(
|
||||
switchMap((params) => {
|
||||
this.providerId = params.providerId;
|
||||
return combineLatest([
|
||||
this.providerService.get(this.providerId),
|
||||
this.consolidatedBillingEnabled$,
|
||||
]).pipe(
|
||||
concatMap(([provider, consolidatedBillingEnabled]) => {
|
||||
if (
|
||||
consolidatedBillingEnabled &&
|
||||
provider.providerStatus === ProviderStatusType.Billable
|
||||
) {
|
||||
return this.providerService.get$(this.providerId).pipe(
|
||||
canAccessBilling(this.configService),
|
||||
map((canAccessBilling) => {
|
||||
if (canAccessBilling) {
|
||||
return from(
|
||||
this.router.navigate(["../manage-client-organizations"], {
|
||||
relativeTo: this.activatedRoute,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<bit-layout variant="secondary">
|
||||
<nav slot="sidebar" *ngIf="provider">
|
||||
<nav slot="sidebar" *ngIf="provider$ | async as provider">
|
||||
<a routerLink="." class="tw-m-5 tw-mt-7 tw-block" [appA11yTitle]="'providerPortal' | i18n">
|
||||
<bit-icon [icon]="logo"></bit-icon>
|
||||
</a>
|
||||
|
@ -7,14 +7,14 @@
|
|||
<bit-nav-item
|
||||
icon="bwi-bank"
|
||||
[text]="'clients' | i18n"
|
||||
[route]="
|
||||
(enableConsolidatedBilling$ | async) &&
|
||||
provider.providerStatus === ProviderStatusType.Billable
|
||||
? 'manage-client-organizations'
|
||||
: 'clients'
|
||||
"
|
||||
[route]="(canAccessBilling$ | async) ? 'manage-client-organizations' : 'clients'"
|
||||
></bit-nav-item>
|
||||
<bit-nav-group icon="bwi-sliders" [text]="'manage' | i18n" route="manage" *ngIf="showManageTab">
|
||||
<bit-nav-group
|
||||
icon="bwi-sliders"
|
||||
[text]="'manage' | i18n"
|
||||
route="manage"
|
||||
*ngIf="showManageTab(provider)"
|
||||
>
|
||||
<bit-nav-item
|
||||
[text]="'people' | i18n"
|
||||
route="manage/people"
|
||||
|
@ -26,13 +26,20 @@
|
|||
*ngIf="provider.useEvents"
|
||||
></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
<bit-nav-group
|
||||
icon="bwi-billing"
|
||||
[text]="'billing' | i18n"
|
||||
route="billing"
|
||||
*ngIf="canAccessBilling$ | async"
|
||||
>
|
||||
<bit-nav-item [text]="'subscription' | i18n" route="billing/subscription"></bit-nav-item>
|
||||
</bit-nav-group>
|
||||
<bit-nav-item
|
||||
icon="bwi-cogs"
|
||||
[text]="'settings' | i18n"
|
||||
route="settings"
|
||||
*ngIf="showSettingsTab"
|
||||
*ngIf="showSettingsTab(provider)"
|
||||
></bit-nav-item>
|
||||
|
||||
<app-toggle-width></app-toggle-width>
|
||||
</nav>
|
||||
<app-payment-method-warnings
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { CommonModule } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute, RouterModule } from "@angular/router";
|
||||
import { switchMap, Observable, Subject, filter, startWith } from "rxjs";
|
||||
import { takeUntil } from "rxjs/operators";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { canAccessBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { IconModule, LayoutComponent, NavigationModule } from "@bitwarden/components";
|
||||
|
@ -28,21 +30,17 @@ import { ToggleWidthComponent } from "@bitwarden/web-vault/app/layouts/toggle-wi
|
|||
ToggleWidthComponent,
|
||||
],
|
||||
})
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||
export class ProvidersLayoutComponent {
|
||||
export class ProvidersLayoutComponent implements OnInit, OnDestroy {
|
||||
protected readonly logo = ProviderPortalLogo;
|
||||
|
||||
provider: Provider;
|
||||
private providerId: string;
|
||||
private destroy$ = new Subject<void>();
|
||||
protected provider$: Observable<Provider>;
|
||||
protected canAccessBilling$: Observable<boolean>;
|
||||
|
||||
protected showPaymentMethodWarningBanners$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.ShowPaymentMethodWarningBanners,
|
||||
);
|
||||
|
||||
protected enableConsolidatedBilling$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.EnableConsolidatedBilling,
|
||||
);
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private providerService: ProviderService,
|
||||
|
@ -51,37 +49,30 @@ export class ProvidersLayoutComponent {
|
|||
|
||||
ngOnInit() {
|
||||
document.body.classList.remove("layout_frontend");
|
||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||
this.route.params.subscribe(async (params) => {
|
||||
this.providerId = params.providerId;
|
||||
await this.load();
|
||||
});
|
||||
|
||||
this.provider$ = this.route.params.pipe(
|
||||
switchMap((params) => this.providerService.get$(params.providerId)),
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
|
||||
this.canAccessBilling$ = this.provider$.pipe(
|
||||
filter((provider) => !!provider),
|
||||
canAccessBilling(this.configService),
|
||||
startWith(false),
|
||||
takeUntil(this.destroy$),
|
||||
);
|
||||
}
|
||||
|
||||
async load() {
|
||||
this.provider = await this.providerService.get(this.providerId);
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
|
||||
get showMenuBar() {
|
||||
return this.showManageTab || this.showSettingsTab;
|
||||
showManageTab(provider: Provider) {
|
||||
return provider.canManageUsers || provider.canAccessEventLogs;
|
||||
}
|
||||
|
||||
get showManageTab() {
|
||||
return this.provider.canManageUsers || this.provider.canAccessEventLogs;
|
||||
showSettingsTab(provider: Provider) {
|
||||
return provider.isProviderAdmin;
|
||||
}
|
||||
|
||||
get showSettingsTab() {
|
||||
return this.provider.isProviderAdmin;
|
||||
}
|
||||
|
||||
get manageRoute(): string {
|
||||
switch (true) {
|
||||
case this.provider.canManageUsers:
|
||||
return "manage/people";
|
||||
case this.provider.canAccessEventLogs:
|
||||
return "manage/events";
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly ProviderStatusType = ProviderStatusType;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@ import { ProvidersComponent } from "@bitwarden/web-vault/app/admin-console/provi
|
|||
import { FrontendLayoutComponent } from "@bitwarden/web-vault/app/layouts/frontend-layout.component";
|
||||
import { UserLayoutComponent } from "@bitwarden/web-vault/app/layouts/user-layout.component";
|
||||
|
||||
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients/manage-client-organizations.component";
|
||||
import { ProviderSubscriptionComponent, hasConsolidatedBilling } from "../../billing/providers";
|
||||
import { ManageClientOrganizationsComponent } from "../../billing/providers/clients";
|
||||
|
||||
import { ClientsComponent } from "./clients/clients.component";
|
||||
import { CreateOrganizationComponent } from "./clients/create-organization.component";
|
||||
|
@ -68,6 +69,7 @@ const routes: Routes = [
|
|||
{ path: "clients", component: ClientsComponent, data: { titleId: "clients" } },
|
||||
{
|
||||
path: "manage-client-organizations",
|
||||
canActivate: [hasConsolidatedBilling],
|
||||
component: ManageClientOrganizationsComponent,
|
||||
data: { titleId: "clients" },
|
||||
},
|
||||
|
@ -99,6 +101,25 @@ const routes: Routes = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "billing",
|
||||
canActivate: [hasConsolidatedBilling],
|
||||
data: { providerPermissions: (provider: Provider) => provider.isProviderAdmin },
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
pathMatch: "full",
|
||||
redirectTo: "subscription",
|
||||
},
|
||||
{
|
||||
path: "subscription",
|
||||
component: ProviderSubscriptionComponent,
|
||||
data: {
|
||||
titleId: "subscription",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "settings",
|
||||
children: [
|
||||
|
|
|
@ -9,6 +9,7 @@ import { OrganizationPlansComponent, TaxInfoComponent } from "@bitwarden/web-vau
|
|||
import { PaymentMethodWarningsModule } from "@bitwarden/web-vault/app/billing/shared";
|
||||
import { OssModule } from "@bitwarden/web-vault/app/oss.module";
|
||||
|
||||
import { ProviderSubscriptionComponent } from "../../billing/providers";
|
||||
import {
|
||||
CreateClientOrganizationComponent,
|
||||
ManageClientOrganizationSubscriptionComponent,
|
||||
|
@ -62,6 +63,7 @@ import { SetupComponent } from "./setup/setup.component";
|
|||
CreateClientOrganizationComponent,
|
||||
ManageClientOrganizationsComponent,
|
||||
ManageClientOrganizationSubscriptionComponent,
|
||||
ProviderSubscriptionComponent,
|
||||
],
|
||||
providers: [WebProviderService, ProviderPermissionsGuard],
|
||||
})
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute, Router } from "@angular/router";
|
||||
import { combineLatest, firstValueFrom, from, lastValueFrom } from "rxjs";
|
||||
import { concatMap, switchMap, takeUntil } from "rxjs/operators";
|
||||
import { firstValueFrom, from, lastValueFrom, map } from "rxjs";
|
||||
import { switchMap, takeUntil } from "rxjs/operators";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { ProviderUserType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { ProviderOrganizationOrganizationDetailsResponse } from "@bitwarden/common/admin-console/models/response/provider/provider-organization.response";
|
||||
import { BillingApiServiceAbstraction as BillingApiService } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||
import { canAccessBilling } from "@bitwarden/common/billing/abstractions/provider-billing.service.abstraction";
|
||||
import { PlanResponse } from "@bitwarden/common/billing/models/response/plan.response";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
|
||||
|
@ -36,10 +36,6 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
|
|||
loading = true;
|
||||
manageOrganizations = false;
|
||||
|
||||
private consolidatedBillingEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.EnableConsolidatedBilling,
|
||||
);
|
||||
|
||||
protected plans: PlanResponse[];
|
||||
|
||||
constructor(
|
||||
|
@ -72,23 +68,16 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
|
|||
.pipe(
|
||||
switchMap((params) => {
|
||||
this.providerId = params.providerId;
|
||||
return combineLatest([
|
||||
this.providerService.get(this.providerId),
|
||||
this.consolidatedBillingEnabled$,
|
||||
]).pipe(
|
||||
concatMap(([provider, consolidatedBillingEnabled]) => {
|
||||
if (
|
||||
!consolidatedBillingEnabled ||
|
||||
provider.providerStatus !== ProviderStatusType.Billable
|
||||
) {
|
||||
return this.providerService.get$(this.providerId).pipe(
|
||||
canAccessBilling(this.configService),
|
||||
map((canAccessBilling) => {
|
||||
if (!canAccessBilling) {
|
||||
return from(
|
||||
this.router.navigate(["../clients"], {
|
||||
relativeTo: this.activatedRoute,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
this.provider = provider;
|
||||
this.manageOrganizations = this.provider.type === ProviderUserType.ProviderAdmin;
|
||||
return from(this.load());
|
||||
}
|
||||
}),
|
||||
|
@ -104,6 +93,10 @@ export class ManageClientOrganizationsComponent extends BaseClientsComponent {
|
|||
}
|
||||
|
||||
async load() {
|
||||
this.provider = await firstValueFrom(this.providerService.get$(this.providerId));
|
||||
|
||||
this.manageOrganizations = this.provider.type === ProviderUserType.ProviderAdmin;
|
||||
|
||||
this.clients = (await this.apiService.getProviderClients(this.providerId)).data;
|
||||
|
||||
this.dataSource.data = this.clients;
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import { inject } from "@angular/core";
|
||||
import { ActivatedRouteSnapshot, CanActivateFn, createUrlTreeFromSnapshot } from "@angular/router";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service";
|
||||
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
export const hasConsolidatedBilling: CanActivateFn = async (route: ActivatedRouteSnapshot) => {
|
||||
const configService = inject(ConfigService);
|
||||
const providerService = inject(ProviderService);
|
||||
|
||||
const provider = await firstValueFrom(providerService.get$(route.params.providerId));
|
||||
|
||||
const consolidatedBillingEnabled = await configService.getFeatureFlag(
|
||||
FeatureFlag.EnableConsolidatedBilling,
|
||||
);
|
||||
|
||||
if (
|
||||
!consolidatedBillingEnabled ||
|
||||
!provider ||
|
||||
!provider.isProviderAdmin ||
|
||||
provider.providerStatus !== ProviderStatusType.Billable
|
||||
) {
|
||||
return createUrlTreeFromSnapshot(route, ["/providers", route.params.providerId]);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./guards/has-consolidated-billing.guard";
|
||||
export * from "./provider-subscription.component";
|
|
@ -0,0 +1 @@
|
|||
<app-header></app-header>
|
|
@ -0,0 +1,7 @@
|
|||
import { Component } from "@angular/core";
|
||||
|
||||
@Component({
|
||||
selector: "app-provider-subscription",
|
||||
templateUrl: "./provider-subscription.component.html",
|
||||
})
|
||||
export class ProviderSubscriptionComponent {}
|
|
@ -117,13 +117,12 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||
return super.logInTwoFactor(twoFactor);
|
||||
}
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
|
||||
const authRequestCredentials = this.cache.value.authRequestCredentials;
|
||||
if (
|
||||
authRequestCredentials.decryptedMasterKey &&
|
||||
authRequestCredentials.decryptedMasterKeyHash
|
||||
) {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setMasterKey(
|
||||
authRequestCredentials.decryptedMasterKey,
|
||||
userId,
|
||||
|
@ -147,15 +146,14 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||
if (authRequestCredentials.decryptedUserKey) {
|
||||
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
|
||||
} else {
|
||||
await this.trySetUserKeyWithMasterKey();
|
||||
await this.trySetUserKeyWithMasterKey(userId);
|
||||
|
||||
// Establish trust if required after setting user key
|
||||
await this.deviceTrustService.trustDeviceIfRequired(userId);
|
||||
}
|
||||
}
|
||||
|
||||
private async trySetUserKeyWithMasterKey(): Promise<void> {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
|
|
|
@ -244,6 +244,7 @@ describe("LoginStrategy", () => {
|
|||
const result = await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(result).toEqual({
|
||||
userId: userId,
|
||||
forcePasswordReset: ForceSetPasswordReason.AdminForcePasswordReset,
|
||||
resetMasterPassword: true,
|
||||
twoFactorProviders: null,
|
||||
|
|
|
@ -241,6 +241,7 @@ export abstract class LoginStrategy {
|
|||
|
||||
// Must come before setting keys, user key needs email to update additional keys
|
||||
const userId = await this.saveAccountInformation(response);
|
||||
result.userId = userId;
|
||||
|
||||
if (response.twoFactorToken != null) {
|
||||
// note: we can read email from access token b/c it was saved in saveAccountInformation
|
||||
|
@ -249,7 +250,7 @@ export abstract class LoginStrategy {
|
|||
await this.tokenService.setTwoFactorToken(userEmail, response.twoFactorToken);
|
||||
}
|
||||
|
||||
await this.setMasterKey(response);
|
||||
await this.setMasterKey(response, userId);
|
||||
await this.setUserKey(response, userId);
|
||||
await this.setPrivateKey(response);
|
||||
|
||||
|
@ -259,7 +260,7 @@ export abstract class LoginStrategy {
|
|||
}
|
||||
|
||||
// The keys comes from different sources depending on the login strategy
|
||||
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
|
||||
protected abstract setMasterKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||
protected abstract setUserKey(response: IdentityTokenResponse, userId: UserId): Promise<void>;
|
||||
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
||||
|
||||
|
|
|
@ -173,8 +173,11 @@ describe("PasswordLoginStrategy", () => {
|
|||
localHashedPassword,
|
||||
userId,
|
||||
);
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||
tokenResponse.key,
|
||||
userId,
|
||||
);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(tokenResponse.privateKey);
|
||||
});
|
||||
|
||||
|
|
|
@ -147,6 +147,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||
|
||||
const [authResult, identityResponse] = await this.startLogIn();
|
||||
|
||||
if (identityResponse instanceof IdentityCaptchaResponse) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
const masterPasswordPolicyOptions =
|
||||
this.getMasterPasswordPolicyOptionsFromResponse(identityResponse);
|
||||
|
||||
|
@ -157,23 +161,23 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||
credentials,
|
||||
masterPasswordPolicyOptions,
|
||||
);
|
||||
if (meetsRequirements) {
|
||||
return authResult;
|
||||
}
|
||||
|
||||
if (!meetsRequirements) {
|
||||
if (authResult.requiresCaptcha || authResult.requiresTwoFactor) {
|
||||
// Save the flag to this strategy for later use as the master password is about to pass out of scope
|
||||
this.cache.next({
|
||||
...this.cache.value,
|
||||
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
|
||||
});
|
||||
} else {
|
||||
// Authentication was successful, save the force update password options with the state service
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
userId,
|
||||
);
|
||||
authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
|
||||
}
|
||||
if (identityResponse instanceof IdentityTwoFactorResponse) {
|
||||
// Save the flag to this strategy for use in 2fa login as the master password is about to pass out of scope
|
||||
this.cache.next({
|
||||
...this.cache.value,
|
||||
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
|
||||
});
|
||||
} else {
|
||||
// Authentication was successful, save the force update password options with the state service
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ForceSetPasswordReason.WeakMasterPassword,
|
||||
authResult.userId, // userId is only available on successful login
|
||||
);
|
||||
authResult.forcePasswordReset = ForceSetPasswordReason.WeakMasterPassword;
|
||||
}
|
||||
}
|
||||
return authResult;
|
||||
|
@ -196,17 +200,18 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||
!result.requiresCaptcha &&
|
||||
forcePasswordResetReason != ForceSetPasswordReason.None
|
||||
) {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setForceSetPasswordReason(forcePasswordResetReason, userId);
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
forcePasswordResetReason,
|
||||
result.userId,
|
||||
);
|
||||
result.forcePasswordReset = forcePasswordResetReason;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
|
||||
const { masterKey, localMasterKeyHash } = this.cache.value;
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
await this.masterPasswordService.setMasterKeyHash(localMasterKeyHash, userId);
|
||||
}
|
||||
|
@ -219,12 +224,12 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||
if (this.encryptionKeyMigrationRequired(response)) {
|
||||
return;
|
||||
}
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key, userId);
|
||||
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
await this.cryptoService.setUserKey(userKey, userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -239,9 +244,9 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||
}
|
||||
|
||||
private getMasterPasswordPolicyOptionsFromResponse(
|
||||
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse,
|
||||
response: IdentityTokenResponse | IdentityTwoFactorResponse,
|
||||
): MasterPasswordPolicyOptions {
|
||||
if (response == null || response instanceof IdentityCaptchaResponse) {
|
||||
if (response == null) {
|
||||
return null;
|
||||
}
|
||||
return MasterPasswordPolicyOptions.fromResponse(response.masterPasswordPolicy);
|
||||
|
|
|
@ -163,7 +163,10 @@ describe("SsoLoginStrategy", () => {
|
|||
|
||||
// Assert
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(tokenResponse.key);
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||
tokenResponse.key,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
describe("Trusted Device Decryption", () => {
|
||||
|
@ -417,7 +420,7 @@ describe("SsoLoginStrategy", () => {
|
|||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId);
|
||||
});
|
||||
|
||||
it("converts new SSO user with no master password to Key Connector on first login", async () => {
|
||||
|
@ -430,6 +433,7 @@ describe("SsoLoginStrategy", () => {
|
|||
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||
tokenResponse,
|
||||
ssoOrgId,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -468,7 +472,7 @@ describe("SsoLoginStrategy", () => {
|
|||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId);
|
||||
});
|
||||
|
||||
it("converts new SSO user with no master password to Key Connector on first login", async () => {
|
||||
|
@ -481,6 +485,7 @@ describe("SsoLoginStrategy", () => {
|
|||
expect(keyConnectorService.convertNewSsoUserToKeyConnector).toHaveBeenCalledWith(
|
||||
tokenResponse,
|
||||
ssoOrgId,
|
||||
userId,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-con
|
|||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
|
||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||
|
@ -124,7 +125,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||
this.ssoEmail2FaSessionToken$ = this.cache.pipe(map((state) => state.ssoEmail2FaSessionToken));
|
||||
}
|
||||
|
||||
async logIn(credentials: SsoLoginCredentials) {
|
||||
async logIn(credentials: SsoLoginCredentials): Promise<AuthResult> {
|
||||
const data = new SsoLoginStrategyData();
|
||||
data.orgId = credentials.orgId;
|
||||
|
||||
|
@ -147,10 +148,9 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||
|
||||
// Auth guard currently handles redirects for this.
|
||||
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setForceSetPasswordReason(
|
||||
ssoAuthResult.forcePasswordReset,
|
||||
userId,
|
||||
ssoAuthResult.userId,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -163,7 +163,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||
return ssoAuthResult;
|
||||
}
|
||||
|
||||
protected override async setMasterKey(tokenResponse: IdentityTokenResponse) {
|
||||
protected override async setMasterKey(tokenResponse: IdentityTokenResponse, userId: UserId) {
|
||||
// The only way we can be setting a master key at this point is if we are using Key Connector.
|
||||
// First, check to make sure that we should do so based on the token response.
|
||||
if (this.shouldSetMasterKeyFromKeyConnector(tokenResponse)) {
|
||||
|
@ -175,10 +175,11 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(
|
||||
tokenResponse,
|
||||
this.cache.value.orgId,
|
||||
userId,
|
||||
);
|
||||
} else {
|
||||
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
|
||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -231,7 +232,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||
|
||||
if (masterKeyEncryptedUserKey) {
|
||||
// set the master key encrypted user key if it exists
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey);
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId);
|
||||
}
|
||||
|
||||
const userDecryptionOptions = tokenResponse?.userDecryptionOptions;
|
||||
|
@ -251,7 +252,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||
this.getKeyConnectorUrl(tokenResponse) != null
|
||||
) {
|
||||
// Key connector enabled for user
|
||||
await this.trySetUserKeyWithMasterKey();
|
||||
await this.trySetUserKeyWithMasterKey(userId);
|
||||
}
|
||||
|
||||
// Note: In the traditional SSO flow with MP without key connector, the lock component
|
||||
|
@ -338,8 +339,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||
}
|
||||
}
|
||||
|
||||
private async trySetUserKeyWithMasterKey(): Promise<void> {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
|
||||
// There is a scenario in which the master key is not set here. That will occur if the user
|
||||
|
|
|
@ -174,7 +174,7 @@ describe("UserApiLoginStrategy", () => {
|
|||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl);
|
||||
expect(keyConnectorService.setMasterKeyFromUrl).toHaveBeenCalledWith(keyConnectorUrl, userId);
|
||||
});
|
||||
|
||||
it("decrypts and sets the user key if Key Connector is enabled", async () => {
|
||||
|
@ -195,6 +195,6 @@ describe("UserApiLoginStrategy", () => {
|
|||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -93,11 +93,11 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
|||
return authResult;
|
||||
}
|
||||
|
||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
|
||||
if (response.apiUseKeyConnector) {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const keyConnectorUrl = env.getKeyConnectorUrl();
|
||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl, userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,11 +108,10 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
|||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||
|
||||
if (response.apiUseKeyConnector) {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
await this.cryptoService.setUserKey(userKey, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +122,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
|||
);
|
||||
}
|
||||
|
||||
// Overridden to save client ID and secret to token service
|
||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse): Promise<UserId> {
|
||||
const userId = await super.saveAccountInformation(tokenResponse);
|
||||
|
||||
|
|
|
@ -208,7 +208,10 @@ describe("WebAuthnLoginStrategy", () => {
|
|||
// Assert
|
||||
// Master key encrypted user key should be set
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(idTokenResponse.key);
|
||||
expect(cryptoService.setMasterKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||
idTokenResponse.key,
|
||||
userId,
|
||||
);
|
||||
|
||||
expect(cryptoService.decryptToBytes).toHaveBeenCalledTimes(1);
|
||||
expect(cryptoService.decryptToBytes).toHaveBeenCalledWith(
|
||||
|
@ -220,7 +223,7 @@ describe("WebAuthnLoginStrategy", () => {
|
|||
idTokenResponse.userDecryptionOptions.webAuthnPrfOption.encryptedUserKey.encryptedString,
|
||||
mockPrfPrivateKey,
|
||||
);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockUserKey, userId);
|
||||
expect(cryptoService.setPrivateKey).toHaveBeenCalledWith(idTokenResponse.privateKey);
|
||||
|
||||
// Master key and private key should not be set
|
||||
|
|
|
@ -98,7 +98,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
|||
throw new Error("2FA not supported yet for WebAuthn Login.");
|
||||
}
|
||||
|
||||
protected override async setMasterKey() {
|
||||
protected override async setMasterKey(response: IdentityTokenResponse, userId: UserId) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
|
@ -107,7 +107,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
|||
|
||||
if (masterKeyEncryptedUserKey) {
|
||||
// set the master key encrypted user key if it exists
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey);
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(masterKeyEncryptedUserKey, userId);
|
||||
}
|
||||
|
||||
const userDecryptionOptions = idTokenResponse?.userDecryptionOptions;
|
||||
|
@ -134,7 +134,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
|||
);
|
||||
|
||||
if (userKey) {
|
||||
await this.cryptoService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey);
|
||||
await this.cryptoService.setUserKey(new SymmetricCryptoKey(userKey) as UserKey, userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { Observable } from "rxjs";
|
||||
|
||||
import { UserId } from "../../types/guid";
|
||||
import { ProviderData } from "../models/data/provider.data";
|
||||
import { Provider } from "../models/domain/provider";
|
||||
|
||||
export abstract class ProviderService {
|
||||
get$: (id: string) => Observable<Provider>;
|
||||
get: (id: string) => Promise<Provider>;
|
||||
getAll: () => Promise<Provider[]>;
|
||||
save: (providers: { [id: string]: ProviderData }, userId?: UserId) => Promise<any>;
|
||||
|
|
|
@ -25,7 +25,13 @@ export class EncryptedOrganizationKey implements BaseEncryptedOrganizationKey {
|
|||
constructor(private key: string) {}
|
||||
|
||||
async decrypt(cryptoService: CryptoService) {
|
||||
const decValue = await cryptoService.rsaDecrypt(this.key);
|
||||
const activeUserPrivateKey = await cryptoService.getPrivateKey();
|
||||
|
||||
if (activeUserPrivateKey == null) {
|
||||
throw new Error("Active user does not have a private key, cannot decrypt organization key.");
|
||||
}
|
||||
|
||||
const decValue = await cryptoService.rsaDecrypt(this.key, activeUserPrivateKey);
|
||||
return new SymmetricCryptoKey(decValue) as OrgKey;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { FakeAccountService, FakeStateProvider, mockAccountServiceWith } from "../../../spec";
|
||||
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
|
@ -86,6 +88,7 @@ describe("ProviderService", () => {
|
|||
fakeStateProvider = new FakeStateProvider(fakeAccountService);
|
||||
fakeUserState = fakeStateProvider.singleUser.getFake(fakeUserId, PROVIDERS);
|
||||
fakeActiveUserState = fakeStateProvider.activeUser.getFake(PROVIDERS);
|
||||
|
||||
providerService = new ProviderService(fakeStateProvider);
|
||||
});
|
||||
|
||||
|
@ -106,6 +109,22 @@ describe("ProviderService", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("get$()", () => {
|
||||
it("Returns an observable of a single provider from state that matches the specified id", async () => {
|
||||
const mockData = buildMockProviders(5);
|
||||
fakeUserState.nextState(arrayToRecord(mockData));
|
||||
const result = providerService.get$(mockData[3].id);
|
||||
const provider = await firstValueFrom(result);
|
||||
expect(provider).toEqual(new Provider(mockData[3]));
|
||||
});
|
||||
|
||||
it("Returns an observable of undefined if the specified provider is not found", async () => {
|
||||
const result = providerService.get$("this-provider-does-not-exist");
|
||||
const provider = await firstValueFrom(result);
|
||||
expect(provider).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("get()", () => {
|
||||
it("Returns a single provider from state that matches the specified id", async () => {
|
||||
const mockData = buildMockProviders(5);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Observable, map, firstValueFrom, of, switchMap, take } from "rxjs";
|
||||
import { firstValueFrom, map, Observable, of, switchMap, take } from "rxjs";
|
||||
|
||||
import { UserKeyDefinition, PROVIDERS_DISK, StateProvider } from "../../platform/state";
|
||||
import { PROVIDERS_DISK, StateProvider, UserKeyDefinition } from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { ProviderService as ProviderServiceAbstraction } from "../abstractions/provider.service";
|
||||
import { ProviderData } from "../models/data/provider.data";
|
||||
|
@ -38,6 +38,10 @@ export class ProviderService implements ProviderServiceAbstraction {
|
|||
);
|
||||
}
|
||||
|
||||
get$(id: string): Observable<Provider> {
|
||||
return this.providers$().pipe(mapToSingleProvider(id));
|
||||
}
|
||||
|
||||
async get(id: string): Promise<Provider> {
|
||||
return await firstValueFrom(this.providers$().pipe(mapToSingleProvider(id)));
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { Organization } from "../../admin-console/models/domain/organization";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||
|
||||
export abstract class KeyConnectorService {
|
||||
setMasterKeyFromUrl: (url?: string) => Promise<void>;
|
||||
setMasterKeyFromUrl: (url: string, userId: UserId) => Promise<void>;
|
||||
getManagingOrganization: () => Promise<Organization>;
|
||||
getUsesKeyConnector: () => Promise<boolean>;
|
||||
migrateUser: () => Promise<void>;
|
||||
|
@ -10,6 +11,7 @@ export abstract class KeyConnectorService {
|
|||
convertNewSsoUserToKeyConnector: (
|
||||
tokenResponse: IdentityTokenResponse,
|
||||
orgId: string,
|
||||
userId: UserId,
|
||||
) => Promise<void>;
|
||||
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
|
||||
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { TwoFactorProviderType } from "../../enums/two-factor-provider-type";
|
||||
|
||||
import { ForceSetPasswordReason } from "./force-set-password-reason";
|
||||
|
||||
export class AuthResult {
|
||||
userId: UserId;
|
||||
captchaSiteKey = "";
|
||||
// TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal
|
||||
/**
|
||||
|
|
|
@ -215,7 +215,7 @@ describe("KeyConnectorService", () => {
|
|||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||
|
||||
// Act
|
||||
await keyConnectorService.setMasterKeyFromUrl(url);
|
||||
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
|
||||
|
||||
// Assert
|
||||
expect(apiService.getMasterKeyFromKeyConnector).toHaveBeenCalledWith(url);
|
||||
|
@ -235,7 +235,7 @@ describe("KeyConnectorService", () => {
|
|||
|
||||
try {
|
||||
// Act
|
||||
await keyConnectorService.setMasterKeyFromUrl(url);
|
||||
await keyConnectorService.setMasterKeyFromUrl(url, mockUserId);
|
||||
} catch {
|
||||
// Assert
|
||||
expect(logService.error).toHaveBeenCalledWith(error);
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
} from "../../platform/state";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { MasterKey } from "../../types/key";
|
||||
import { AccountService } from "../abstractions/account.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
|
||||
|
@ -100,12 +101,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||
}
|
||||
|
||||
// TODO: UserKey should be renamed to MasterKey and typed accordingly
|
||||
async setMasterKeyFromUrl(url: string) {
|
||||
async setMasterKeyFromUrl(url: string, userId: UserId) {
|
||||
try {
|
||||
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(url);
|
||||
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
|
||||
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
} catch (e) {
|
||||
this.handleKeyConnectorError(e);
|
||||
|
@ -123,7 +123,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||
);
|
||||
}
|
||||
|
||||
async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) {
|
||||
async convertNewSsoUserToKeyConnector(
|
||||
tokenResponse: IdentityTokenResponse,
|
||||
orgId: string,
|
||||
userId: UserId,
|
||||
) {
|
||||
// TODO: Remove after tokenResponse.keyConnectorUrl is deprecated in 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537)
|
||||
const {
|
||||
kdf,
|
||||
|
@ -145,12 +149,11 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||
kdfConfig,
|
||||
);
|
||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(masterKey.encKeyB64);
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
|
||||
const userKey = await this.cryptoService.makeUserKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey[0]);
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString);
|
||||
await this.cryptoService.setUserKey(userKey[0], userId);
|
||||
await this.cryptoService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId);
|
||||
|
||||
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
|
||||
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
import { map, Observable, OperatorFunction, switchMap } from "rxjs";
|
||||
|
||||
import { ProviderStatusType } from "@bitwarden/common/admin-console/enums";
|
||||
import { Provider } from "@bitwarden/common/admin-console/models/domain/provider";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
type MaybeProvider = Provider | undefined;
|
||||
|
||||
export const canAccessBilling = (
|
||||
configService: ConfigService,
|
||||
): OperatorFunction<MaybeProvider, boolean> =>
|
||||
switchMap<MaybeProvider, Observable<boolean>>((provider) =>
|
||||
configService
|
||||
.getFeatureFlag$(FeatureFlag.EnableConsolidatedBilling)
|
||||
.pipe(
|
||||
map((consolidatedBillingEnabled) =>
|
||||
provider
|
||||
? provider.isProviderAdmin &&
|
||||
provider.providerStatus === ProviderStatusType.Billable &&
|
||||
consolidatedBillingEnabled
|
||||
: false,
|
||||
),
|
||||
),
|
||||
);
|
|
@ -311,15 +311,17 @@ export abstract class CryptoService {
|
|||
* @param data The data to encrypt
|
||||
* @param publicKey The public key to use for encryption, if not provided, the user's public key will be used
|
||||
* @returns The encrypted data
|
||||
* @throws If the given publicKey is a null-ish value.
|
||||
*/
|
||||
abstract rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise<EncString>;
|
||||
abstract rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString>;
|
||||
/**
|
||||
* Decrypts a value using RSA.
|
||||
* @param encValue The encrypted value to decrypt
|
||||
* @param privateKeyValue The private key to use for decryption
|
||||
* @param privateKey The private key to use for decryption
|
||||
* @returns The decrypted value
|
||||
* @throws If the given privateKey is a null-ish value.
|
||||
*/
|
||||
abstract rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise<Uint8Array>;
|
||||
abstract rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise<Uint8Array>;
|
||||
abstract randomNumber(min: number, max: number): Promise<number>;
|
||||
/**
|
||||
* Generates a new cipher key
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { Observable } from "rxjs";
|
||||
|
||||
import { BiometricKey } from "../../auth/types/biometric-key";
|
||||
import { GeneratorOptions } from "../../tools/generator/generator-options";
|
||||
import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/generator/password";
|
||||
|
@ -24,8 +22,6 @@ export type InitOptions = {
|
|||
};
|
||||
|
||||
export abstract class StateService<T extends Account = Account> {
|
||||
accounts$: Observable<{ [userId: string]: T }>;
|
||||
|
||||
addAccount: (account: T) => Promise<void>;
|
||||
clearDecryptedData: (userId: UserId) => Promise<void>;
|
||||
clean: (options?: StorageOptions) => Promise<void>;
|
||||
|
|
|
@ -621,19 +621,20 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
await this.stateProvider.setUserState(USER_EVER_HAD_USER_KEY, null, userId);
|
||||
}
|
||||
|
||||
async rsaEncrypt(data: Uint8Array, publicKey?: Uint8Array): Promise<EncString> {
|
||||
async rsaEncrypt(data: Uint8Array, publicKey: Uint8Array): Promise<EncString> {
|
||||
if (publicKey == null) {
|
||||
publicKey = await this.getPublicKey();
|
||||
}
|
||||
if (publicKey == null) {
|
||||
throw new Error("Public key unavailable.");
|
||||
throw new Error("'publicKey' is a required parameter and must be non-null");
|
||||
}
|
||||
|
||||
const encBytes = await this.cryptoFunctionService.rsaEncrypt(data, publicKey, "sha1");
|
||||
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Utils.fromBufferToB64(encBytes));
|
||||
}
|
||||
|
||||
async rsaDecrypt(encValue: string, privateKeyValue?: Uint8Array): Promise<Uint8Array> {
|
||||
async rsaDecrypt(encValue: string, privateKey: Uint8Array): Promise<Uint8Array> {
|
||||
if (privateKey == null) {
|
||||
throw new Error("'privateKey' is a required parameter and must be non-null");
|
||||
}
|
||||
|
||||
const headerPieces = encValue.split(".");
|
||||
let encType: EncryptionType = null;
|
||||
let encPieces: string[];
|
||||
|
@ -665,10 +666,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
}
|
||||
|
||||
const data = Utils.fromB64ToArray(encPieces[0]);
|
||||
const privateKey = privateKeyValue ?? (await this.getPrivateKey());
|
||||
if (privateKey == null) {
|
||||
throw new Error("No private key.");
|
||||
}
|
||||
|
||||
let alg: "sha1" | "sha256" = "sha1";
|
||||
switch (encType) {
|
||||
|
@ -771,6 +768,13 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
publicKey: string;
|
||||
privateKey: EncString;
|
||||
}> {
|
||||
// Verify user key doesn't exist
|
||||
const existingUserKey = await this.getUserKey();
|
||||
if (existingUserKey != null) {
|
||||
this.logService.error("Tried to initialize account with existing user key.");
|
||||
throw new Error("Cannot initialize account, keys already exist.");
|
||||
}
|
||||
|
||||
const userKey = (await this.keyGenerationService.createKey(512)) as UserKey;
|
||||
const [publicKey, privateKey] = await this.makeKeyPair(userKey);
|
||||
await this.setUserKey(userKey);
|
||||
|
|
|
@ -65,6 +65,10 @@ describe("derived decrypted org keys", () => {
|
|||
"org-id-2": new SymmetricCryptoKey(makeStaticByteArray(64, 2)) as OrgKey,
|
||||
};
|
||||
|
||||
const userPrivateKey = makeStaticByteArray(64, 3);
|
||||
|
||||
cryptoService.getPrivateKey.mockResolvedValue(userPrivateKey);
|
||||
|
||||
// TODO: How to not have to mock these decryptions. They are internal concerns of EncryptedOrganizationKey
|
||||
cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-1"].key);
|
||||
cryptoService.rsaDecrypt.mockResolvedValueOnce(decryptedOrgKeys["org-id-2"].key);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BehaviorSubject, firstValueFrom, map } from "rxjs";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { Jsonify, JsonValue } from "type-fest";
|
||||
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
|
@ -52,9 +52,6 @@ export class StateService<
|
|||
TAccount extends Account = Account,
|
||||
> implements StateServiceAbstraction<TAccount>
|
||||
{
|
||||
protected accountsSubject = new BehaviorSubject<{ [userId: string]: TAccount }>({});
|
||||
accounts$ = this.accountsSubject.asObservable();
|
||||
|
||||
private hasBeenInited = false;
|
||||
protected isRecoveredSession = false;
|
||||
|
||||
|
@ -115,8 +112,6 @@ export class StateService<
|
|||
state = await this.syncAccountFromDisk(authenticatedAccounts[i]);
|
||||
}
|
||||
|
||||
await this.pushAccounts();
|
||||
|
||||
return state;
|
||||
});
|
||||
}
|
||||
|
@ -153,7 +148,6 @@ export class StateService<
|
|||
|
||||
await this.removeAccountFromDisk(options?.userId);
|
||||
await this.removeAccountFromMemory(options?.userId);
|
||||
await this.pushAccounts();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -856,7 +850,6 @@ export class StateService<
|
|||
});
|
||||
});
|
||||
}
|
||||
await this.pushAccounts();
|
||||
}
|
||||
|
||||
protected async scaffoldNewAccountStorage(account: TAccount): Promise<void> {
|
||||
|
@ -934,17 +927,6 @@ export class StateService<
|
|||
);
|
||||
}
|
||||
|
||||
protected async pushAccounts(): Promise<void> {
|
||||
await this.state().then((state) => {
|
||||
if (state.accounts == null || Object.keys(state.accounts).length < 1) {
|
||||
this.accountsSubject.next({});
|
||||
return;
|
||||
}
|
||||
|
||||
this.accountsSubject.next(state.accounts);
|
||||
});
|
||||
}
|
||||
|
||||
protected reconcileOptions(
|
||||
requestedOptions: StorageOptions,
|
||||
defaultOptions: StorageOptions,
|
||||
|
@ -1096,8 +1078,6 @@ export class StateService<
|
|||
|
||||
return state;
|
||||
});
|
||||
|
||||
await this.pushAccounts();
|
||||
}
|
||||
|
||||
protected createAccount(init: Partial<TAccount> = null): TAccount {
|
||||
|
|
Loading…
Reference in New Issue