1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-15 15:53:27 +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

@@ -10,7 +10,6 @@ import { OrganizationData } from "../../../admin-console/models/data/organizatio
import { Organization } from "../../../admin-console/models/domain/organization";
import { ProfileOrganizationResponse } from "../../../admin-console/models/response/profile-organization.response";
import { KeyConnectorUserKeyResponse } from "../../../auth/models/response/key-connector-user-key.response";
import { FakeMasterPasswordService } from "../../../auth/services/master-password/fake-master-password.service";
import { TokenService } from "../../../auth/services/token.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { Utils } from "../../../platform/misc/utils";
@@ -18,6 +17,7 @@ import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-cr
import { KeyGenerationService } from "../../../platform/services/key-generation.service";
import { OrganizationId, UserId } from "../../../types/guid";
import { MasterKey } from "../../../types/key";
import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service";
import { KeyConnectorUserKeyRequest } from "../models/key-connector-user-key.request";
import {

View File

@@ -16,7 +16,6 @@ import { OrganizationService } from "../../../admin-console/abstractions/organiz
import { OrganizationUserType } from "../../../admin-console/enums";
import { Organization } from "../../../admin-console/models/domain/organization";
import { AccountService } from "../../../auth/abstractions/account.service";
import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction";
import { TokenService } from "../../../auth/abstractions/token.service";
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
import { KeysRequest } from "../../../models/request/keys.request";
@@ -32,6 +31,7 @@ import {
} 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";

View File

@@ -0,0 +1,96 @@
import { Observable } from "rxjs";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { EncString } from "../../../platform/models/domain/enc-string";
import { UserId } from "../../../types/guid";
import { MasterKey, UserKey } from "../../../types/key";
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

@@ -0,0 +1,74 @@
// 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 { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
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";
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

@@ -0,0 +1,205 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { firstValueFrom, map, Observable } from "rxjs";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
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 { EncryptService } from "../../crypto/abstractions/encrypt.service";
import { InternalMasterPasswordServiceAbstraction } from "../abstractions/master-password.service.abstraction";
/** 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

@@ -12,7 +12,6 @@ import { SearchService } from "../../../abstractions/search.service";
import { AccountInfo } from "../../../auth/abstractions/account.service";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { FakeMasterPasswordService } from "../../../auth/services/master-password/fake-master-password.service";
import { LogService } from "../../../platform/abstractions/log.service";
import { MessagingService } from "../../../platform/abstractions/messaging.service";
import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service";
@@ -23,6 +22,7 @@ import { StateEventRunnerService } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { FolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
import { FakeMasterPasswordService } from "../../master-password/services/fake-master-password.service";
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";
import { VaultTimeout, VaultTimeoutStringType } from "../types/vault-timeout.type";

View File

@@ -9,7 +9,6 @@ import { BiometricsService } from "@bitwarden/key-management";
import { SearchService } from "../../../abstractions/search.service";
import { AccountService } from "../../../auth/abstractions/account.service";
import { AuthService } from "../../../auth/abstractions/auth.service";
import { InternalMasterPasswordServiceAbstraction } from "../../../auth/abstractions/master-password.service.abstraction";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { LogService } from "../../../platform/abstractions/log.service";
import { MessagingService } from "../../../platform/abstractions/messaging.service";
@@ -20,6 +19,7 @@ import { StateEventRunnerService } from "../../../platform/state";
import { UserId } from "../../../types/guid";
import { CipherService } from "../../../vault/abstractions/cipher.service";
import { FolderService } from "../../../vault/abstractions/folder/folder.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "../../master-password/abstractions/master-password.service.abstraction";
import { VaultTimeoutSettingsService } from "../abstractions/vault-timeout-settings.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../abstractions/vault-timeout.service";
import { VaultTimeoutAction } from "../enums/vault-timeout-action.enum";