1
0
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:
Thomas Avery
2025-03-13 13:29:27 -05:00
committed by GitHub
parent 4f724974e9
commit 81335978d8
59 changed files with 74 additions and 74 deletions

View File

@@ -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>;
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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";

View File

@@ -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";