mirror of
https://github.com/bitwarden/browser
synced 2026-02-14 07:23:45 +00:00
* move pinKeyEncryptedUserKey * move pinKeyEncryptedUserKeyEphemeral * remove comments, move docs * cleanup * use UserKeyDefinition * refactor methods * add migration * fix browser dependency * add tests for migration * rename to pinService * move state to PinService * add PinService dep to CryptoService * move protectedPin to state provider * update service deps * renaming * move decryptUserKeyWithPin to pinService * update service injection * move more methods our of crypto service * remove CryptoService dep from PinService and update service injection * remove cryptoService reference * add method to FakeMasterPasswordService * fix circular dependency * fix desktop service injection * update browser dependencies * add protectedPin to migrations * move storePinKey to pinService * update and clarify documentation * more jsdoc updates * update import paths * refactor isPinLockSet method * update state definitions * initialize service before injecting into other services * initialize service before injecting into other services (bw.ts) * update clearOn and do additional cleanup * clarify docs and naming * assign abstract & private methods, add clarity to decryptAndMigrateOldPinKeyEncryptedMasterKey() method * derived state (attempt) * fix typos * use accountService to get active user email * use constant userId * add derived state * add get and clear for oldPinKeyEncryptedMasterKey * require userId * move pinProtected * add clear methods * remove pinProtected from account.ts and replace methods * add methods to create and store pinKeyEncryptedUserKey * add pinProtected/oldPinKeyEncrypterMasterKey to migration * update migration tests * update migration rollback tests * update to systemService and decryptAndMigrate... method * remove old test * increase length of state definition name to meet test requirements * rename 'TRANSIENT' to 'EPHEMERAL' for consistency * fix tests for login strategies, vault-export, and fake MP service * more updates to login-strategy tests * write new tests for core pinKeyEncrypterUserKey methods and isPinSet * write new tests for pinProtected and oldPinKeyEncryptedMasterKey methods * minor test reformatting * update test for decryptUserKeyWithPin() * fix bug with oldPinKeyEncryptedMasterKey * fix tests for vault-timeout-settings.service * fix bitwarden-password-protected-importer test * fix login strategy tests and auth-request.service test * update pinService tests * fix crypto service tests * add jsdoc * fix test file import * update jsdocs for decryptAndMigrateOldPinKeyEncryptedMasterKey() * update error messages and jsdocs * add null checks, move userId retrievals * update migration tests * update stateService calls to require userId * update test for decryptUserKeyWithPin() * update oldPinKeyEncryptedMasterKey migration tests * more test updates * fix factory import * update tests for isPinSet() and createProtectedPin() * add test for makePinKey() * add test for createPinKeyEncryptedUserKey() * add tests for getPinLockType() * consolidate userId verification tests * add tests for storePinKeyEncryptedUserKey() * fix service dep * get email based on userId * use MasterPasswordService instead of internal * rename protectedPin to userKeyEncryptedPin * rename to pinKeyEncryptedUserKeyPersistent * update method params * fix CryptoService tests * jsdoc update * use EncString for userKeyEncryptedPin * remove comment * use cryptoFunctionService.compareFast() * update tests * cleanup, remove comments * resolve merge conflict * fix DI of MasterPasswordService * more DI fixes
234 lines
8.2 KiB
TypeScript
234 lines
8.2 KiB
TypeScript
import { Observable, Subject, firstValueFrom } from "rxjs";
|
|
import { Jsonify } from "type-fest";
|
|
|
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
|
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
|
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
|
|
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
|
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
|
import {
|
|
AUTH_REQUEST_DISK_LOCAL,
|
|
StateProvider,
|
|
UserKeyDefinition,
|
|
} from "@bitwarden/common/platform/state";
|
|
import { UserId } from "@bitwarden/common/types/guid";
|
|
import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
|
|
|
import { AuthRequestServiceAbstraction } from "../../abstractions/auth-request.service.abstraction";
|
|
|
|
/**
|
|
* Disk-local to maintain consistency between tabs (even though
|
|
* approvals are currently only available on desktop). We don't
|
|
* want to clear this on logout as it's a user preference.
|
|
*/
|
|
export const ACCEPT_AUTH_REQUESTS_KEY = new UserKeyDefinition<boolean>(
|
|
AUTH_REQUEST_DISK_LOCAL,
|
|
"acceptAuthRequests",
|
|
{
|
|
deserializer: (value) => value ?? false,
|
|
clearOn: [],
|
|
},
|
|
);
|
|
|
|
/**
|
|
* Disk-local to maintain consistency between tabs. We don't want to
|
|
* clear this on logout since admin auth requests are long-lived.
|
|
*/
|
|
export const ADMIN_AUTH_REQUEST_KEY = new UserKeyDefinition<Jsonify<AdminAuthRequestStorable>>(
|
|
AUTH_REQUEST_DISK_LOCAL,
|
|
"adminAuthRequest",
|
|
{
|
|
deserializer: (value) => value,
|
|
clearOn: [],
|
|
},
|
|
);
|
|
|
|
export class AuthRequestService implements AuthRequestServiceAbstraction {
|
|
private authRequestPushNotificationSubject = new Subject<string>();
|
|
authRequestPushNotification$: Observable<string>;
|
|
|
|
constructor(
|
|
private appIdService: AppIdService,
|
|
private accountService: AccountService,
|
|
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
|
private cryptoService: CryptoService,
|
|
private apiService: ApiService,
|
|
private stateProvider: StateProvider,
|
|
) {
|
|
this.authRequestPushNotification$ = this.authRequestPushNotificationSubject.asObservable();
|
|
}
|
|
|
|
async getAcceptAuthRequests(userId: UserId): Promise<boolean> {
|
|
if (userId == null) {
|
|
throw new Error("User ID is required");
|
|
}
|
|
|
|
const value = await firstValueFrom(
|
|
this.stateProvider.getUser(userId, ACCEPT_AUTH_REQUESTS_KEY).state$,
|
|
);
|
|
return value;
|
|
}
|
|
|
|
async setAcceptAuthRequests(accept: boolean, userId: UserId): Promise<void> {
|
|
if (userId == null) {
|
|
throw new Error("User ID is required");
|
|
}
|
|
|
|
await this.stateProvider.setUserState(ACCEPT_AUTH_REQUESTS_KEY, accept, userId);
|
|
}
|
|
|
|
async getAdminAuthRequest(userId: UserId): Promise<AdminAuthRequestStorable | null> {
|
|
if (userId == null) {
|
|
throw new Error("User ID is required");
|
|
}
|
|
|
|
const authRequestSerialized = await firstValueFrom(
|
|
this.stateProvider.getUser(userId, ADMIN_AUTH_REQUEST_KEY).state$,
|
|
);
|
|
const adminAuthRequestStorable = AdminAuthRequestStorable.fromJSON(authRequestSerialized);
|
|
return adminAuthRequestStorable;
|
|
}
|
|
|
|
async setAdminAuthRequest(authRequest: AdminAuthRequestStorable, userId: UserId): Promise<void> {
|
|
if (userId == null) {
|
|
throw new Error("User ID is required");
|
|
}
|
|
if (authRequest == null) {
|
|
throw new Error("Auth request is required");
|
|
}
|
|
|
|
await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, authRequest.toJSON(), userId);
|
|
}
|
|
|
|
async clearAdminAuthRequest(userId: UserId): Promise<void> {
|
|
if (userId == null) {
|
|
throw new Error("User ID is required");
|
|
}
|
|
|
|
await this.stateProvider.setUserState(ADMIN_AUTH_REQUEST_KEY, null, userId);
|
|
}
|
|
|
|
async approveOrDenyAuthRequest(
|
|
approve: boolean,
|
|
authRequest: AuthRequestResponse,
|
|
): Promise<AuthRequestResponse> {
|
|
if (!authRequest.id) {
|
|
throw new Error("Auth request has no id");
|
|
}
|
|
if (!authRequest.publicKey) {
|
|
throw new Error("Auth request has no public key");
|
|
}
|
|
const pubKey = Utils.fromB64ToArray(authRequest.publicKey);
|
|
|
|
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
|
const masterKeyHash = await firstValueFrom(this.masterPasswordService.masterKeyHash$(userId));
|
|
let encryptedMasterKeyHash;
|
|
let keyToEncrypt;
|
|
|
|
if (masterKey && masterKeyHash) {
|
|
// Only encrypt the master password hash if masterKey exists as
|
|
// we won't have a masterKeyHash without a masterKey
|
|
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
|
|
Utils.fromUtf8ToArray(masterKeyHash),
|
|
pubKey,
|
|
);
|
|
keyToEncrypt = masterKey.encKey;
|
|
} else {
|
|
const userKey = await this.cryptoService.getUserKey();
|
|
keyToEncrypt = userKey.key;
|
|
}
|
|
|
|
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
|
|
|
|
const response = new PasswordlessAuthRequest(
|
|
encryptedKey.encryptedString,
|
|
encryptedMasterKeyHash?.encryptedString,
|
|
await this.appIdService.getAppId(),
|
|
approve,
|
|
);
|
|
return await this.apiService.putAuthRequest(authRequest.id, response);
|
|
}
|
|
|
|
async setUserKeyAfterDecryptingSharedUserKey(
|
|
authReqResponse: AuthRequestResponse,
|
|
authReqPrivateKey: Uint8Array,
|
|
) {
|
|
const userKey = await this.decryptPubKeyEncryptedUserKey(
|
|
authReqResponse.key,
|
|
authReqPrivateKey,
|
|
);
|
|
await this.cryptoService.setUserKey(userKey);
|
|
}
|
|
|
|
async setKeysAfterDecryptingSharedMasterKeyAndHash(
|
|
authReqResponse: AuthRequestResponse,
|
|
authReqPrivateKey: Uint8Array,
|
|
) {
|
|
const { masterKey, masterKeyHash } = await this.decryptPubKeyEncryptedMasterKeyAndHash(
|
|
authReqResponse.key,
|
|
authReqResponse.masterPasswordHash,
|
|
authReqPrivateKey,
|
|
);
|
|
|
|
// Decrypt and set user key in state
|
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
|
|
|
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
|
|
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
|
await this.masterPasswordService.setMasterKeyHash(masterKeyHash, userId);
|
|
|
|
await this.cryptoService.setUserKey(userKey);
|
|
}
|
|
|
|
// Decryption helpers
|
|
async decryptPubKeyEncryptedUserKey(
|
|
pubKeyEncryptedUserKey: string,
|
|
privateKey: Uint8Array,
|
|
): Promise<UserKey> {
|
|
const decryptedUserKeyBytes = await this.cryptoService.rsaDecrypt(
|
|
pubKeyEncryptedUserKey,
|
|
privateKey,
|
|
);
|
|
|
|
return new SymmetricCryptoKey(decryptedUserKeyBytes) as UserKey;
|
|
}
|
|
|
|
async decryptPubKeyEncryptedMasterKeyAndHash(
|
|
pubKeyEncryptedMasterKey: string,
|
|
pubKeyEncryptedMasterKeyHash: string,
|
|
privateKey: Uint8Array,
|
|
): Promise<{ masterKey: MasterKey; masterKeyHash: string }> {
|
|
const decryptedMasterKeyArrayBuffer = await this.cryptoService.rsaDecrypt(
|
|
pubKeyEncryptedMasterKey,
|
|
privateKey,
|
|
);
|
|
|
|
const decryptedMasterKeyHashArrayBuffer = await this.cryptoService.rsaDecrypt(
|
|
pubKeyEncryptedMasterKeyHash,
|
|
privateKey,
|
|
);
|
|
|
|
const masterKey = new SymmetricCryptoKey(decryptedMasterKeyArrayBuffer) as MasterKey;
|
|
const masterKeyHash = Utils.fromBufferToUtf8(decryptedMasterKeyHashArrayBuffer);
|
|
|
|
return {
|
|
masterKey,
|
|
masterKeyHash,
|
|
};
|
|
}
|
|
|
|
sendAuthRequestPushNotification(notification: AuthRequestPushNotification): void {
|
|
if (notification.id != null) {
|
|
this.authRequestPushNotificationSubject.next(notification.id);
|
|
}
|
|
}
|
|
}
|