mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
* 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>
201 lines
8.0 KiB
TypeScript
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,
|
|
);
|
|
}
|
|
}
|