1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 21:33:27 +00:00
Files
browser/libs/common/src/key-management/key-connector/services/key-connector.service.ts
Maciej Zieniuk 239556b55f [PM-18017] Show key connector domain in remove password page (#14695)
* Passed in userId on RemovePasswordComponent.

* Added userId on other references to KeyConnectorService methods

* remove password component refactor, test coverage, enabled strict

* explicit user id provided to key connector service

* redirect to / instead when user not logged in or not managing organization

* key connector service explicit user id

* key connector service no longer requires account service

* key connector service missing null type

* cli convert to key connector unit tests

* remove unnecessary SyncService

* error toast not showing on ErrorResponse

* bad import due to merge conflict

* bad import due to merge conflict

* missing loading in remove password component for browser extension

* error handling in remove password component

* organization observable race condition in key-connector

* usesKeyConnector always returns boolean

* unit test coverage

* key connector reactive

* reactive key connector service

* introducing convertAccountRequired$

* cli build fix

* moving message sending side effect to sync

* key connector service unit tests

* fix unit tests

* move key connector components to KM team ownership

* new unit tests in wrong place

* key connector domain shown in remove password component

* type safety improvements

* convert to key connector command localization

* key connector domain in convert to key connector command

* convert to key connector command unit tests with prompt assert

* organization name placement change in the remove password component

* unit test update

* key connector url required to be provided when migrating user

* unit tests in wrong place after KM code ownership move

* infinite page reload

* failing unit tests

* failing unit tests

---------

Co-authored-by: Todd Martin <tmartin@bitwarden.com>
2025-05-19 08:58:51 -04:00

201 lines
8.0 KiB
TypeScript

// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { combineLatest, filter, firstValueFrom, Observable, of, switchMap } from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import {
Argon2KdfConfig,
KdfConfig,
PBKDF2KdfConfig,
KeyService,
KdfType,
} from "@bitwarden/key-management";
import { ApiService } from "../../../abstractions/api.service";
import { OrganizationService } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
import { OrganizationUserType } from "../../../admin-console/enums";
import { Organization } from "../../../admin-console/models/domain/organization";
import { TokenService } from "../../../auth/abstractions/token.service";
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
import { KeysRequest } from "../../../models/request/keys.request";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { Utils } from "../../../platform/misc/utils";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { KEY_CONNECTOR_DISK, StateProvider, UserKeyDefinition } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { MasterKey } from "../../../types/key";
import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";
import { SetKeyConnectorKeyRequest } from "../models/set-key-connector-key.request";
export const USES_KEY_CONNECTOR = new UserKeyDefinition<boolean | null>(
KEY_CONNECTOR_DISK,
"usesKeyConnector",
{
deserializer: (usesKeyConnector) => usesKeyConnector,
clearOn: ["logout"],
cleanupDelayMs: 0,
},
);
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
readonly convertAccountRequired$: Observable<boolean>;
constructor(
accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
private keyService: KeyService,
private apiService: ApiService,
private tokenService: TokenService,
private logService: LogService,
private organizationService: OrganizationService,
private keyGenerationService: KeyGenerationService,
private logoutCallback: (logoutReason: LogoutReason, userId?: string) => Promise<void>,
private stateProvider: StateProvider,
) {
this.convertAccountRequired$ = accountService.activeAccount$.pipe(
filter((account) => account != null),
switchMap((account) =>
combineLatest([
of(account.id),
this.organizationService
.organizations$(account.id)
.pipe(filter((organizations) => organizations != null)),
this.stateProvider
.getUserState$(USES_KEY_CONNECTOR, account.id)
.pipe(filter((usesKeyConnector) => usesKeyConnector != null)),
tokenService.hasAccessToken$(account.id).pipe(filter((hasToken) => hasToken)),
]),
),
switchMap(async ([userId, organizations, usesKeyConnector]) => {
const loggedInUsingSso = await this.tokenService.getIsExternal(userId);
const requiredByOrganization = this.findManagingOrganization(organizations) != null;
const userIsNotUsingKeyConnector = !usesKeyConnector;
return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector;
}),
);
}
async setUsesKeyConnector(usesKeyConnector: boolean, userId: UserId) {
await this.stateProvider.getUser(userId, USES_KEY_CONNECTOR).update(() => usesKeyConnector);
}
async getUsesKeyConnector(userId: UserId): Promise<boolean> {
return (
(await firstValueFrom(this.stateProvider.getUserState$(USES_KEY_CONNECTOR, userId))) ?? false
);
}
async migrateUser(keyConnectorUrl: string, userId: UserId) {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
);
try {
await this.apiService.postUserKeyToKeyConnector(keyConnectorUrl, keyConnectorRequest);
} catch (e) {
this.handleKeyConnectorError(e);
}
await this.apiService.postConvertToKeyConnector();
await this.setUsesKeyConnector(true, userId);
}
// TODO: UserKey should be renamed to MasterKey and typed accordingly
async setMasterKeyFromUrl(keyConnectorUrl: string, userId: UserId) {
try {
const masterKeyResponse = await this.apiService.getMasterKeyFromKeyConnector(keyConnectorUrl);
const keyArr = Utils.fromB64ToArray(masterKeyResponse.key);
const masterKey = new SymmetricCryptoKey(keyArr) as MasterKey;
await this.masterPasswordService.setMasterKey(masterKey, userId);
} catch (e) {
this.handleKeyConnectorError(e);
}
}
async getManagingOrganization(userId: UserId): Promise<Organization> {
const organizations = await firstValueFrom(this.organizationService.organizations$(userId));
return this.findManagingOrganization(organizations);
}
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,
kdfIterations,
kdfMemory,
kdfParallelism,
keyConnectorUrl: legacyKeyConnectorUrl,
userDecryptionOptions,
} = tokenResponse;
const password = await this.keyGenerationService.createKey(512);
const kdfConfig: KdfConfig =
kdf === KdfType.PBKDF2_SHA256
? new PBKDF2KdfConfig(kdfIterations)
: new Argon2KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
const masterKey = await this.keyService.makeMasterKey(
password.keyB64,
await this.tokenService.getEmail(),
kdfConfig,
);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(
Utils.fromBufferToB64(masterKey.inner().encryptionKey),
);
await this.masterPasswordService.setMasterKey(masterKey, userId);
const userKey = await this.keyService.makeUserKey(masterKey);
await this.keyService.setUserKey(userKey[0], userId);
await this.keyService.setMasterKeyEncryptedUserKey(userKey[1].encryptedString, userId);
const [pubKey, privKey] = await this.keyService.makeKeyPair(userKey[0]);
try {
const keyConnectorUrl =
legacyKeyConnectorUrl ?? userDecryptionOptions?.keyConnectorOption?.keyConnectorUrl;
await this.apiService.postUserKeyToKeyConnector(keyConnectorUrl, keyConnectorRequest);
} catch (e) {
this.handleKeyConnectorError(e);
}
const keys = new KeysRequest(pubKey, privKey.encryptedString);
const setPasswordRequest = new SetKeyConnectorKeyRequest(
userKey[1].encryptedString,
kdfConfig,
orgId,
keys,
);
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
}
private handleKeyConnectorError(e: any) {
this.logService.error(e);
if (this.logoutCallback != null) {
// 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.logoutCallback("keyConnectorError");
}
throw new Error("Key Connector error");
}
private findManagingOrganization(organizations: Organization[]): Organization | undefined {
return organizations.find(
(o) =>
o.keyConnectorEnabled &&
o.type !== OrganizationUserType.Admin &&
o.type !== OrganizationUserType.Owner &&
!o.isProviderUser,
);
}
}