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:
@@ -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 {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user