mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 00:03:56 +00:00
[PM-17669] Move MasterPasswordService to KM (#13148)
* Move MasterPasswordService to KM
This commit is contained in:
@@ -1,96 +0,0 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { EncString } from "../../platform/models/domain/enc-string";
|
||||
import { UserId } from "../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../types/key";
|
||||
import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
|
||||
|
||||
export abstract class MasterPasswordServiceAbstraction {
|
||||
/**
|
||||
* An observable that emits if the user is being forced to set a password on login and why.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
abstract forceSetPasswordReason$: (userId: UserId) => Observable<ForceSetPasswordReason>;
|
||||
/**
|
||||
* An observable that emits the master key for the user.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
abstract masterKey$: (userId: UserId) => Observable<MasterKey>;
|
||||
/**
|
||||
* An observable that emits the master key hash for the user.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
abstract masterKeyHash$: (userId: UserId) => Observable<string>;
|
||||
/**
|
||||
* Returns the master key encrypted user key for the user.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise<EncString>;
|
||||
/**
|
||||
* Decrypts the user key with the provided master key
|
||||
* @param masterKey The user's master key
|
||||
* * @param userId The desired user
|
||||
* @param userKey The user's encrypted symmetric key
|
||||
* @throws If either the MasterKey or UserKey are not resolved, or if the UserKey encryption type
|
||||
* is neither AesCbc256_B64 nor AesCbc256_HmacSha256_B64
|
||||
* @returns The user key
|
||||
*/
|
||||
abstract decryptUserKeyWithMasterKey: (
|
||||
masterKey: MasterKey,
|
||||
userId: string,
|
||||
userKey?: EncString,
|
||||
) => Promise<UserKey>;
|
||||
}
|
||||
|
||||
export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction {
|
||||
/**
|
||||
* Set the master key for the user.
|
||||
* Note: Use {@link clearMasterKey} to clear the master key.
|
||||
* @param masterKey The master key.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID or master key is missing.
|
||||
*/
|
||||
abstract setMasterKey: (masterKey: MasterKey, userId: UserId) => Promise<void>;
|
||||
/**
|
||||
* Clear the master key for the user.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
abstract clearMasterKey: (userId: UserId) => Promise<void>;
|
||||
/**
|
||||
* Set the master key hash for the user.
|
||||
* Note: Use {@link clearMasterKeyHash} to clear the master key hash.
|
||||
* @param masterKeyHash The master key hash.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID or master key hash is missing.
|
||||
*/
|
||||
abstract setMasterKeyHash: (masterKeyHash: string, userId: UserId) => Promise<void>;
|
||||
/**
|
||||
* Clear the master key hash for the user.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID is missing.
|
||||
*/
|
||||
abstract clearMasterKeyHash: (userId: UserId) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Set the master key encrypted user key for the user.
|
||||
* @param encryptedKey The master key encrypted user key.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID or encrypted key is missing.
|
||||
*/
|
||||
abstract setMasterKeyEncryptedUserKey: (encryptedKey: EncString, userId: UserId) => Promise<void>;
|
||||
/**
|
||||
* Set the force set password reason for the user.
|
||||
* @param reason The reason the user is being forced to set a password.
|
||||
* @param userId The user ID.
|
||||
* @throws If the user ID or reason is missing.
|
||||
*/
|
||||
abstract setForceSetPasswordReason: (
|
||||
reason: ForceSetPasswordReason,
|
||||
userId: UserId,
|
||||
) => Promise<void>;
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { ReplaySubject, Observable } from "rxjs";
|
||||
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
|
||||
|
||||
export class FakeMasterPasswordService implements InternalMasterPasswordServiceAbstraction {
|
||||
mock = mock<InternalMasterPasswordServiceAbstraction>();
|
||||
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
|
||||
masterKeySubject = new ReplaySubject<MasterKey | null>(1);
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
|
||||
masterKeyHashSubject = new ReplaySubject<string | null>(1);
|
||||
// eslint-disable-next-line rxjs/no-exposed-subjects -- test class
|
||||
forceSetPasswordReasonSubject = new ReplaySubject<ForceSetPasswordReason>(1);
|
||||
|
||||
constructor(initialMasterKey?: MasterKey, initialMasterKeyHash?: string) {
|
||||
this.masterKeySubject.next(initialMasterKey);
|
||||
this.masterKeyHashSubject.next(initialMasterKeyHash);
|
||||
}
|
||||
|
||||
masterKey$(userId: UserId): Observable<MasterKey> {
|
||||
return this.masterKeySubject.asObservable();
|
||||
}
|
||||
|
||||
setMasterKey(masterKey: MasterKey, userId: UserId): Promise<void> {
|
||||
return this.mock.setMasterKey(masterKey, userId);
|
||||
}
|
||||
|
||||
clearMasterKey(userId: UserId): Promise<void> {
|
||||
return this.mock.clearMasterKey(userId);
|
||||
}
|
||||
|
||||
masterKeyHash$(userId: UserId): Observable<string> {
|
||||
return this.masterKeyHashSubject.asObservable();
|
||||
}
|
||||
|
||||
getMasterKeyEncryptedUserKey(userId: UserId): Promise<EncString> {
|
||||
return this.mock.getMasterKeyEncryptedUserKey(userId);
|
||||
}
|
||||
|
||||
setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise<void> {
|
||||
return this.mock.setMasterKeyEncryptedUserKey(encryptedKey, userId);
|
||||
}
|
||||
|
||||
setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise<void> {
|
||||
return this.mock.setMasterKeyHash(masterKeyHash, userId);
|
||||
}
|
||||
|
||||
clearMasterKeyHash(userId: UserId): Promise<void> {
|
||||
return this.mock.clearMasterKeyHash(userId);
|
||||
}
|
||||
|
||||
forceSetPasswordReason$(userId: UserId): Observable<ForceSetPasswordReason> {
|
||||
return this.forceSetPasswordReasonSubject.asObservable();
|
||||
}
|
||||
|
||||
setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> {
|
||||
return this.mock.setForceSetPasswordReason(reason, userId);
|
||||
}
|
||||
|
||||
decryptUserKeyWithMasterKey(
|
||||
masterKey: MasterKey,
|
||||
userId: string,
|
||||
userKey?: EncString,
|
||||
): Promise<UserKey> {
|
||||
return this.mock.decryptUserKeyWithMasterKey(masterKey, userId, userKey);
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
import { EncryptService } from "../../../key-management/crypto/abstractions/encrypt.service";
|
||||
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||
import { LogService } from "../../../platform/abstractions/log.service";
|
||||
import { StateService } from "../../../platform/abstractions/state.service";
|
||||
import { EncryptionType } from "../../../platform/enums";
|
||||
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
MASTER_PASSWORD_DISK,
|
||||
MASTER_PASSWORD_MEMORY,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
} from "../../../platform/state";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey, UserKey } from "../../../types/key";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
|
||||
|
||||
/** Memory since master key shouldn't be available on lock */
|
||||
const MASTER_KEY = new UserKeyDefinition<MasterKey>(MASTER_PASSWORD_MEMORY, "masterKey", {
|
||||
deserializer: (masterKey) => SymmetricCryptoKey.fromJSON(masterKey) as MasterKey,
|
||||
clearOn: ["lock", "logout"],
|
||||
});
|
||||
|
||||
/** Disk since master key hash is used for unlock */
|
||||
const MASTER_KEY_HASH = new UserKeyDefinition<string>(MASTER_PASSWORD_DISK, "masterKeyHash", {
|
||||
deserializer: (masterKeyHash) => masterKeyHash,
|
||||
clearOn: ["logout"],
|
||||
});
|
||||
|
||||
/** Disk to persist through lock */
|
||||
const MASTER_KEY_ENCRYPTED_USER_KEY = new UserKeyDefinition<EncryptedString>(
|
||||
MASTER_PASSWORD_DISK,
|
||||
"masterKeyEncryptedUserKey",
|
||||
{
|
||||
deserializer: (key) => key,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
/** Disk to persist through lock and account switches */
|
||||
const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition<ForceSetPasswordReason>(
|
||||
MASTER_PASSWORD_DISK,
|
||||
"forceSetPasswordReason",
|
||||
{
|
||||
deserializer: (reason) => reason,
|
||||
clearOn: ["logout"],
|
||||
},
|
||||
);
|
||||
|
||||
export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction {
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private stateService: StateService,
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private encryptService: EncryptService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
masterKey$(userId: UserId): Observable<MasterKey> {
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
return this.stateProvider.getUser(userId, MASTER_KEY).state$;
|
||||
}
|
||||
|
||||
masterKeyHash$(userId: UserId): Observable<string> {
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
return this.stateProvider.getUser(userId, MASTER_KEY_HASH).state$;
|
||||
}
|
||||
|
||||
forceSetPasswordReason$(userId: UserId): Observable<ForceSetPasswordReason> {
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
return this.stateProvider
|
||||
.getUser(userId, FORCE_SET_PASSWORD_REASON)
|
||||
.state$.pipe(map((reason) => reason ?? ForceSetPasswordReason.None));
|
||||
}
|
||||
|
||||
// TODO: Remove this method and decrypt directly in the service instead
|
||||
async getMasterKeyEncryptedUserKey(userId: UserId): Promise<EncString> {
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
const key = await firstValueFrom(
|
||||
this.stateProvider.getUser(userId, MASTER_KEY_ENCRYPTED_USER_KEY).state$,
|
||||
);
|
||||
return EncString.fromJSON(key);
|
||||
}
|
||||
|
||||
async setMasterKey(masterKey: MasterKey, userId: UserId): Promise<void> {
|
||||
if (masterKey == null) {
|
||||
throw new Error("Master key is required.");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
await this.stateProvider.getUser(userId, MASTER_KEY).update((_) => masterKey);
|
||||
}
|
||||
|
||||
async clearMasterKey(userId: UserId): Promise<void> {
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
await this.stateProvider.getUser(userId, MASTER_KEY).update((_) => null);
|
||||
}
|
||||
|
||||
async setMasterKeyHash(masterKeyHash: string, userId: UserId): Promise<void> {
|
||||
if (masterKeyHash == null) {
|
||||
throw new Error("Master key hash is required.");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
await this.stateProvider.getUser(userId, MASTER_KEY_HASH).update((_) => masterKeyHash);
|
||||
}
|
||||
|
||||
async clearMasterKeyHash(userId: UserId): Promise<void> {
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
await this.stateProvider.getUser(userId, MASTER_KEY_HASH).update((_) => null);
|
||||
}
|
||||
|
||||
async setMasterKeyEncryptedUserKey(encryptedKey: EncString, userId: UserId): Promise<void> {
|
||||
if (encryptedKey == null) {
|
||||
throw new Error("Encrypted Key is required.");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
await this.stateProvider
|
||||
.getUser(userId, MASTER_KEY_ENCRYPTED_USER_KEY)
|
||||
.update((_) => encryptedKey.toJSON() as EncryptedString);
|
||||
}
|
||||
|
||||
async setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> {
|
||||
if (reason == null) {
|
||||
throw new Error("Reason is required.");
|
||||
}
|
||||
if (userId == null) {
|
||||
throw new Error("User ID is required.");
|
||||
}
|
||||
await this.stateProvider.getUser(userId, FORCE_SET_PASSWORD_REASON).update((_) => reason);
|
||||
}
|
||||
|
||||
async decryptUserKeyWithMasterKey(
|
||||
masterKey: MasterKey,
|
||||
userId: UserId,
|
||||
userKey?: EncString,
|
||||
): Promise<UserKey> {
|
||||
userKey ??= await this.getMasterKeyEncryptedUserKey(userId);
|
||||
masterKey ??= await firstValueFrom(this.masterKey$(userId));
|
||||
|
||||
if (masterKey == null) {
|
||||
throw new Error("No master key found.");
|
||||
}
|
||||
|
||||
// Try one more way to get the user key if it still wasn't found.
|
||||
if (userKey == null) {
|
||||
const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({
|
||||
userId: userId,
|
||||
});
|
||||
|
||||
if (deprecatedKey == null) {
|
||||
throw new Error("No encrypted user key found.");
|
||||
}
|
||||
|
||||
userKey = new EncString(deprecatedKey);
|
||||
}
|
||||
|
||||
let decUserKey: Uint8Array;
|
||||
|
||||
if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||
decUserKey = await this.encryptService.decryptToBytes(
|
||||
userKey,
|
||||
masterKey,
|
||||
"Content: User Key; Encrypting Key: Master Key",
|
||||
);
|
||||
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||
const newKey = await this.keyGenerationService.stretchKey(masterKey);
|
||||
decUserKey = await this.encryptService.decryptToBytes(
|
||||
userKey,
|
||||
newKey,
|
||||
"Content: User Key; Encrypting Key: Stretched Master Key",
|
||||
);
|
||||
} else {
|
||||
throw new Error("Unsupported encryption type.");
|
||||
}
|
||||
|
||||
if (decUserKey == null) {
|
||||
this.logService.warning("Failed to decrypt user key with master key.");
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SymmetricCryptoKey(decUserKey) as UserKey;
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,13 @@ import {
|
||||
} from "@bitwarden/key-management";
|
||||
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../../spec";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../../key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { VaultTimeoutSettingsService } from "../../../key-management/vault-timeout";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { HashPurpose } from "../../../platform/enums";
|
||||
import { Utils } from "../../../platform/misc/utils";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { MasterKey } from "../../../types/key";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { VerificationType } from "../../enums/verification-type";
|
||||
import { MasterPasswordPolicyResponse } from "../../models/response/master-password-policy.response";
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
// FIXME: remove `src` and fix import
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../../key-management/master-password/abstractions/master-password.service.abstraction";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
import { HashPurpose } from "../../../platform/enums";
|
||||
import { UserId } from "../../../types/guid";
|
||||
import { AccountService } from "../../abstractions/account.service";
|
||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||
import { UserVerificationApiServiceAbstraction } from "../../abstractions/user-verification/user-verification-api.service.abstraction";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../abstractions/user-verification/user-verification.service.abstraction";
|
||||
import { VerificationType } from "../../enums/verification-type";
|
||||
|
||||
Reference in New Issue
Block a user