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( 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>( AUTH_REQUEST_DISK_LOCAL, "adminAuthRequest", { deserializer: (value) => value, clearOn: [], }, ); export class AuthRequestService implements AuthRequestServiceAbstraction { private authRequestPushNotificationSubject = new Subject(); authRequestPushNotification$: Observable; 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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); } } }