diff --git a/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html index 68e58de1ab..25f857ba56 100644 --- a/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html +++ b/apps/browser/src/auth/popup/login-decryption-options/login-decryption-options.component.html @@ -41,9 +41,6 @@ (click)="approveFromOtherDevice()" type="button" class="btn primary block" - [ngClass]="{ - 'btn-bottom-margin': showReqAdminApprovalBtn || showApproveWithMasterPasswordBtn - }" > {{ "approveFromYourOtherDevice" | i18n }} @@ -51,14 +48,13 @@ *ngIf="showReqAdminApprovalBtn" (click)="requestAdminApproval()" type="button" - class="btn block" - [ngClass]="{ 'btn-bottom-margin': showApproveWithMasterPasswordBtn }" + class="btn block btn-top-margin" > {{ "requestAdminApproval" | i18n }} @@ -50,8 +51,6 @@ bitButton type="button" buttonType="secondary" - [ngClass]="{ 'tw-mb-3': showApproveWithMasterPasswordBtn }" - [appA11yTitle]="'requestAdminApproval' | i18n" > {{ "requestAdminApproval" | i18n }} @@ -62,7 +61,6 @@ bitButton type="button" buttonType="secondary" - [appA11yTitle]="'approveWithMasterPassword' | i18n" > {{ "approveWithMasterPassword" | i18n }} diff --git a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts index 6a1e216a55..c052a3948a 100644 --- a/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts +++ b/apps/web/src/app/auth/login/login-decryption-options/login-decryption-options.component.ts @@ -3,11 +3,11 @@ import { FormBuilder } from "@angular/forms"; import { Router } from "@angular/router"; import { BaseLoginDecryptionOptionsComponent } from "@bitwarden/angular/auth/components/base-login-decryption-options.component"; -import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction"; +import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; @Component({ selector: "web-login-decryption-options", templateUrl: "login-decryption-options.component.html", @@ -15,21 +15,21 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv export class LoginDecryptionOptionsComponent extends BaseLoginDecryptionOptionsComponent { constructor( formBuilder: FormBuilder, - devicesApiService: DevicesApiServiceAbstraction, + devicesService: DevicesServiceAbstraction, stateService: StateService, router: Router, messagingService: MessagingService, - tokenService: TokenService, - loginService: LoginService + loginService: LoginService, + validationService: ValidationService ) { super( formBuilder, - devicesApiService, + devicesService, stateService, router, messagingService, - tokenService, - loginService + loginService, + validationService ); } } diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index 0b1ec0e000..420557664e 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -10,6 +10,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { HttpStatusCode } from "@bitwarden/common/enums"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -37,6 +38,7 @@ export class SsoComponent extends BaseSsoComponent { environmentService: EnvironmentService, passwordGenerationService: PasswordGenerationServiceAbstraction, logService: LogService, + configService: ConfigServiceAbstraction, private orgDomainApiService: OrgDomainApiServiceAbstraction, private loginService: LoginService, private validationService: ValidationService @@ -52,7 +54,8 @@ export class SsoComponent extends BaseSsoComponent { cryptoFunctionService, environmentService, passwordGenerationService, - logService + logService, + configService ); this.redirectUri = window.location.origin + "/sso-connector.html"; this.clientId = "web"; diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 57b384e97c..a37ebeea2c 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -9,6 +9,7 @@ import { LoginService } from "@bitwarden/common/auth/abstractions/login.service" import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -42,7 +43,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { twoFactorService: TwoFactorService, appIdService: AppIdService, private routerService: RouterService, - loginService: LoginService + loginService: LoginService, + configService: ConfigServiceAbstraction ) { super( authService, @@ -57,7 +59,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { logService, twoFactorService, appIdService, - loginService + loginService, + configService ); this.onSuccessfulLoginNavigate = this.goAfterLogIn; } diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts index bf16d2513f..026e1f883a 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/link-sso.component.ts @@ -5,6 +5,7 @@ import { SsoComponent } from "@bitwarden/angular/auth/components/sso.component"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -32,7 +33,8 @@ export class LinkSsoComponent extends SsoComponent implements AfterContentInit { passwordGenerationService: PasswordGenerationServiceAbstraction, stateService: StateService, environmentService: EnvironmentService, - logService: LogService + logService: LogService, + configService: ConfigServiceAbstraction ) { super( authService, @@ -45,7 +47,8 @@ export class LinkSsoComponent extends SsoComponent implements AfterContentInit { cryptoFunctionService, environmentService, passwordGenerationService, - logService + logService, + configService ); this.returnUri = "/settings/organizations"; diff --git a/libs/angular/src/auth/components/base-login-decryption-options.component.ts b/libs/angular/src/auth/components/base-login-decryption-options.component.ts index 7d3913442f..b82c8be818 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options.component.ts @@ -1,22 +1,28 @@ import { Directive, OnDestroy, OnInit } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { Router } from "@angular/router"; -import { Subject } from "rxjs"; +import { Observable, Subject, catchError, forkJoin, from, of, finalize, takeUntil } from "rxjs"; -import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction"; +import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; -import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; -import { DeviceType } from "@bitwarden/common/enums/device-type.enum"; +import { + DesktopDeviceTypes, + DeviceType, + MobileDeviceTypes, +} from "@bitwarden/common/enums/device-type.enum"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account"; -// TODO: replace this base component with a service per latest ADR @Directive() export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { - private componentDestroyed$: Subject = new Subject(); + private destroy$ = new Subject(); - userEmail: string = null; + showApproveFromOtherDeviceBtn: boolean; + showReqAdminApprovalBtn: boolean; + showApproveWithMasterPasswordBtn: boolean; + userEmail: string; rememberDeviceForm = this.formBuilder.group({ rememberDevice: [true], @@ -24,55 +30,99 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { loading = true; - showApproveFromOtherDeviceBtn = false; - showReqAdminApprovalBtn = false; - showApproveWithMasterPasswordBtn = false; - constructor( protected formBuilder: FormBuilder, - protected devicesApiService: DevicesApiServiceAbstraction, + protected devicesService: DevicesServiceAbstraction, protected stateService: StateService, protected router: Router, protected messagingService: MessagingService, - protected tokenService: TokenService, - protected loginService: LoginService + protected loginService: LoginService, + private validationService: ValidationService ) {} - async ngOnInit() { - // Determine if the user has any mobile or desktop devices - // to determine if we should show the approve from other device button - const devicesListResponse = await this.devicesApiService.getDevices(); - for (const device of devicesListResponse.data) { - if ( - device.type === DeviceType.Android || - device.type === DeviceType.iOS || - device.type === DeviceType.AndroidAmazon || - device.type === DeviceType.WindowsDesktop || - device.type === DeviceType.MacOsDesktop || - device.type === DeviceType.LinuxDesktop || - device.type === DeviceType.UWP - ) { - this.showApproveFromOtherDeviceBtn = true; - break; - } - } + ngOnInit() { + // Note: this is probably not a comprehensive write up of all scenarios: - const acctDecryptionOptions: AccountDecryptionOptions = - await this.stateService.getAcctDecryptionOptions(); + // If the TDE feature flag is enabled and TDE is configured for the org that the user is a member of, + // then new and existing users can be redirected here after completing the SSO flow (and 2FA if enabled). - // Get user's email from access token: - this.userEmail = await this.tokenService.getEmail(); + // First we must determine user type (new or existing): - // Show the admin approval btn if user has TDE enabled and the org admin approval policy is set && user email is not null - this.showReqAdminApprovalBtn = - !!acctDecryptionOptions.trustedDeviceOption?.hasAdminApproval && this.userEmail != null; + // New User + // - present user with option to remember the device or not (trust the device) + // - present a continue button to proceed to the vault + // - loadNewUserData() --> will need to load enrollment status and user email address. - this.showApproveWithMasterPasswordBtn = acctDecryptionOptions.hasMasterPassword; + // Existing User + // - Determine if user is an admin with access to account recovery in admin console + // - Determine if user has a MP or not, if not, they must be redirected to set one (see PM-1035) + // - Determine if device is trusted or not via device crypto service (method not yet written) + // - If not trusted, present user with login decryption options (approve from other device, approve with master password, request admin approval) + // - loadUntrustedDeviceData() - // TODO: do I extend the lock guard for the lock screen to prevent the user from getting to the lock screen - // if they do not have a master password set + this.loadUntrustedDeviceData(); + } - this.loading = false; + loadUntrustedDeviceData() { + this.loading = true; + + const mobileAndDesktopDeviceTypes: DeviceType[] = Array.from(MobileDeviceTypes).concat( + Array.from(DesktopDeviceTypes) + ); + + // Note: Each obs must handle error here and protect inner observable b/c we are using forkJoin below + // as per RxJs docs: if any given observable errors at some point, then + // forkJoin will error as well and immediately unsubscribe from the other observables. + const mobileOrDesktopDevicesExistence$ = this.devicesService + .getDevicesExistenceByTypes$(mobileAndDesktopDeviceTypes) + .pipe( + catchError((err: unknown) => { + this.validationService.showError(err); + return of(undefined); + }), + takeUntil(this.destroy$) + ); + + const accountDecryptionOptions$: Observable = from( + this.stateService.getAccountDecryptionOptions() + ).pipe( + catchError((err: unknown) => { + this.validationService.showError(err); + return of(undefined); + }), + takeUntil(this.destroy$) + ); + + const email$ = from(this.stateService.getEmail()).pipe( + catchError((err: unknown) => { + this.validationService.showError(err); + return of(undefined); + }), + takeUntil(this.destroy$) + ); + + forkJoin({ + mobileOrDesktopDevicesExistence: mobileOrDesktopDevicesExistence$, + accountDecryptionOptions: accountDecryptionOptions$, + email: email$, + }) + .pipe( + takeUntil(this.destroy$), + finalize(() => { + this.loading = false; + }) + ) + .subscribe(({ mobileOrDesktopDevicesExistence, accountDecryptionOptions, email }) => { + this.showApproveFromOtherDeviceBtn = mobileOrDesktopDevicesExistence || false; + + this.showReqAdminApprovalBtn = + !!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false; + + this.showApproveWithMasterPasswordBtn = + accountDecryptionOptions?.hasMasterPassword || false; + + this.userEmail = email; + }); } approveFromOtherDevice() { @@ -108,7 +158,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { } ngOnDestroy(): void { - this.componentDestroyed$.next(); - this.componentDestroyed$.complete(); + this.destroy$.next(); + this.destroy$.complete(); } } diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index 1a90038721..382c1d07aa 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -8,6 +8,8 @@ import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceResetPasswordReason } from "@bitwarden/common/auth/models/domain/force-reset-password-reason"; import { SsoLogInCredentials } from "@bitwarden/common/auth/models/domain/log-in-credentials"; import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -15,6 +17,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; @Directive() @@ -32,6 +35,7 @@ export class SsoComponent { protected twoFactorRoute = "2fa"; protected successRoute = "lock"; + protected trustedDeviceEncRoute = "login-initiated"; protected changePasswordRoute = "set-password"; protected forcePasswordResetRoute = "update-temp-password"; protected clientId: string; @@ -50,7 +54,8 @@ export class SsoComponent { protected cryptoFunctionService: CryptoFunctionService, protected environmentService: EnvironmentService, protected passwordGenerationService: PasswordGenerationServiceAbstraction, - protected logService: LogService + protected logService: LogService, + protected configService: ConfigServiceAbstraction ) {} async ngOnInit() { @@ -183,13 +188,8 @@ export class SsoComponent { orgIdFromState ); this.formPromise = this.authService.logIn(credentials); - - // if device is trusted go to success route - // what does it mean for a device to not be trusted: - // -- no device key stored locally in secure storage - // -- no device encrypted user symmetric key from server - const response = await this.formPromise; + if (response.requiresTwoFactor) { if (this.onSuccessfulLoginTwoFactorNavigate != null) { await this.onSuccessfulLoginTwoFactorNavigate(); @@ -202,6 +202,10 @@ export class SsoComponent { }); } } else if (response.resetMasterPassword) { + // TODO: for TDE, we are going to deprecate using response.resetMasterPassword + // and instead rely on accountDecryptionOptions to determine if the user needs to set a password + // Users are allowed to not have a MP if TDE feature enabled + TDE configured. Otherwise, they must set a MP + // src: https://bitwarden.atlassian.net/browse/PM-2759?focusedCommentId=39438 if (this.onSuccessfulLoginChangePasswordNavigate != null) { await this.onSuccessfulLoginChangePasswordNavigate(); } else { @@ -224,7 +228,21 @@ export class SsoComponent { if (this.onSuccessfulLoginNavigate != null) { await this.onSuccessfulLoginNavigate(); } else { - this.router.navigate([this.successRoute]); + const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlagBool( + FeatureFlag.TrustedDeviceEncryption + ); + + const accountDecryptionOptions: AccountDecryptionOptions = + await this.stateService.getAccountDecryptionOptions(); + + if ( + trustedDeviceEncryptionFeatureActive && + accountDecryptionOptions.trustedDeviceOption !== undefined + ) { + this.router.navigate([this.trustedDeviceEncRoute]); + } else { + this.router.navigate([this.successRoute]); + } } } } catch (e) { diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index fa7aa66f9f..e6d64c8afe 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -14,12 +14,15 @@ import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/ide import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request"; import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service"; import { WebAuthnIFrame } from "@bitwarden/common/auth/webauthn-iframe"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; +import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account"; import { CaptchaProtectedComponent } from "./captcha-protected.component"; @@ -44,6 +47,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected loginRoute = "login"; protected successRoute = "vault"; + protected trustedDeviceEncRoute = "login-initiated"; constructor( protected authService: AuthService, @@ -58,7 +62,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected logService: LogService, protected twoFactorService: TwoFactorService, protected appIdService: AppIdService, - protected loginService: LoginService + protected loginService: LoginService, + protected configService: ConfigServiceAbstraction ) { super(environmentService, i18nService, platformUtilsService); this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win); @@ -207,6 +212,10 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI this.onSuccessfulLogin(); } if (response.resetMasterPassword) { + // TODO: for TDE, we are going to deprecate using response.resetMasterPassword + // and instead rely on accountDecryptionOptions to determine if the user needs to set a password + // Users are allowed to not have a MP if TDE feature enabled + TDE configured. Otherwise, they must set a MP + // src: https://bitwarden.atlassian.net/browse/PM-2759?focusedCommentId=39438 this.successRoute = "set-password"; } if (response.forcePasswordReset !== ForceResetPasswordReason.None) { @@ -217,11 +226,28 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI await this.onSuccessfulLoginNavigate(); } else { this.loginService.clearValues(); - this.router.navigate([this.successRoute], { - queryParams: { - identifier: this.identifier, - }, - }); + + const ssoTo2faFlowActive = this.route.snapshot.queryParamMap.get("sso") === "true"; + const trustedDeviceEncryptionFeatureActive = await this.configService.getFeatureFlagBool( + FeatureFlag.TrustedDeviceEncryption + ); + + const accountDecryptionOptions: AccountDecryptionOptions = + await this.stateService.getAccountDecryptionOptions(); + + if ( + ssoTo2faFlowActive && + trustedDeviceEncryptionFeatureActive && + accountDecryptionOptions.trustedDeviceOption !== undefined + ) { + this.router.navigate([this.trustedDeviceEncRoute]); + } else { + this.router.navigate([this.successRoute], { + queryParams: { + identifier: this.identifier, + }, + }); + } } } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 234359a523..87ede0f2b9 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -6,6 +6,7 @@ import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstracti import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; import { DeviceCryptoServiceAbstraction } from "@bitwarden/common/abstractions/device-crypto.service.abstraction"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices-api.service.abstraction"; +import { DevicesServiceAbstraction } from "@bitwarden/common/abstractions/devices/devices.service.abstraction"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; @@ -97,6 +98,7 @@ import { ApiService } from "@bitwarden/common/services/api.service"; import { AuditService } from "@bitwarden/common/services/audit.service"; import { DeviceCryptoService } from "@bitwarden/common/services/device-crypto.service.implementation"; import { DevicesApiServiceImplementation } from "@bitwarden/common/services/devices/devices-api.service.implementation"; +import { DevicesServiceImplementation } from "@bitwarden/common/services/devices/devices.service.implementation"; import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service"; import { EventUploadService } from "@bitwarden/common/services/event/event-upload.service"; import { NotificationsService } from "@bitwarden/common/services/notifications.service"; @@ -676,6 +678,11 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction"; useClass: DevicesApiServiceImplementation, deps: [ApiServiceAbstraction], }, + { + provide: DevicesServiceAbstraction, + useClass: DevicesServiceImplementation, + deps: [DevicesApiServiceAbstraction], + }, { provide: DeviceCryptoServiceAbstraction, useClass: DeviceCryptoService, diff --git a/libs/common/src/abstractions/device-crypto.service.abstraction.ts b/libs/common/src/abstractions/device-crypto.service.abstraction.ts index f720ade9a2..b0d53a359f 100644 --- a/libs/common/src/abstractions/device-crypto.service.abstraction.ts +++ b/libs/common/src/abstractions/device-crypto.service.abstraction.ts @@ -1,8 +1,10 @@ -import { DeviceKey } from "../platform/models/domain/symmetric-crypto-key"; +import { DeviceKey, UserKey } from "../platform/models/domain/symmetric-crypto-key"; import { DeviceResponse } from "./devices/responses/device.response"; export abstract class DeviceCryptoServiceAbstraction { trustDevice: () => Promise; getDeviceKey: () => Promise; + // TODO: update param types when available + decryptUserKey: (encryptedDevicePrivateKey: any, encryptedUserKey: any) => Promise; } diff --git a/libs/common/src/abstractions/devices/devices-api.service.abstraction.ts b/libs/common/src/abstractions/devices/devices-api.service.abstraction.ts index eb32c762fd..726c6a4145 100644 --- a/libs/common/src/abstractions/devices/devices-api.service.abstraction.ts +++ b/libs/common/src/abstractions/devices/devices-api.service.abstraction.ts @@ -1,3 +1,4 @@ +import { DeviceType } from "../../enums"; import { ListResponse } from "../../models/response/list.response"; import { DeviceResponse } from "./responses/device.response"; @@ -8,6 +9,7 @@ export abstract class DevicesApiServiceAbstraction { getDeviceByIdentifier: (deviceIdentifier: string) => Promise; getDevices: () => Promise>; + getDevicesExistenceByTypes: (deviceTypes: DeviceType[]) => Promise; updateTrustedDeviceKeys: ( deviceIdentifier: string, diff --git a/libs/common/src/abstractions/devices/devices.service.abstraction.ts b/libs/common/src/abstractions/devices/devices.service.abstraction.ts new file mode 100644 index 0000000000..ac20d0aef4 --- /dev/null +++ b/libs/common/src/abstractions/devices/devices.service.abstraction.ts @@ -0,0 +1,18 @@ +import { Observable } from "rxjs"; + +import { DeviceType } from "../../enums"; + +import { DeviceView } from "./views/device.view"; + +export abstract class DevicesServiceAbstraction { + getDevices$: () => Observable>; + getDevicesExistenceByTypes$: (deviceTypes: DeviceType[]) => Observable; + getDeviceByIdentifier$: (deviceIdentifier: string) => Observable; + isDeviceKnownForUser$: (email: string, deviceIdentifier: string) => Observable; + updateTrustedDeviceKeys$: ( + deviceIdentifier: string, + devicePublicKeyEncryptedUserKey: string, + userKeyEncryptedDevicePublicKey: string, + deviceKeyEncryptedDevicePrivateKey: string + ) => Observable; +} diff --git a/libs/common/src/abstractions/devices/responses/device.response.ts b/libs/common/src/abstractions/devices/responses/device.response.ts index aae1a23168..46874027cd 100644 --- a/libs/common/src/abstractions/devices/responses/device.response.ts +++ b/libs/common/src/abstractions/devices/responses/device.response.ts @@ -4,15 +4,11 @@ import { BaseResponse } from "../../../models/response/base.response"; export class DeviceResponse extends BaseResponse { id: string; userId: string; - name: number; + name: string; identifier: string; type: DeviceType; creationDate: string; revisionDate: string; - encryptedUserKey: string; - encryptedPublicKey: string; - encryptedPrivateKey: string; - constructor(response: any) { super(response); this.id = this.getResponseProperty("Id"); @@ -22,8 +18,5 @@ export class DeviceResponse extends BaseResponse { this.type = this.getResponseProperty("Type"); this.creationDate = this.getResponseProperty("CreationDate"); this.revisionDate = this.getResponseProperty("RevisionDate"); - this.encryptedUserKey = this.getResponseProperty("EncryptedUserKey"); - this.encryptedPublicKey = this.getResponseProperty("EncryptedPublicKey"); - this.encryptedPrivateKey = this.getResponseProperty("EncryptedPrivateKey"); } } diff --git a/libs/common/src/abstractions/devices/views/device.view.ts b/libs/common/src/abstractions/devices/views/device.view.ts new file mode 100644 index 0000000000..5438aa4de1 --- /dev/null +++ b/libs/common/src/abstractions/devices/views/device.view.ts @@ -0,0 +1,17 @@ +import { DeviceType } from "../../../enums"; +import { View } from "../../../models/view/view"; +import { DeviceResponse } from "../responses/device.response"; + +export class DeviceView implements View { + id: string; + userId: string; + name: string; + identifier: string; + type: DeviceType; + creationDate: string; + revisionDate: string; + + constructor(deviceResponse: DeviceResponse) { + Object.assign(this, deviceResponse); + } +} diff --git a/libs/common/src/auth/login-strategies/sso-login.strategy.ts b/libs/common/src/auth/login-strategies/sso-login.strategy.ts index d29fa83afc..504b95d1b2 100644 --- a/libs/common/src/auth/login-strategies/sso-login.strategy.ts +++ b/libs/common/src/auth/login-strategies/sso-login.strategy.ts @@ -83,6 +83,23 @@ export class SsoLogInStrategy extends LogInStrategy { const newSsoUser = tokenResponse.key == null; if (!newSsoUser) { + // TODO: check if TDE feature flag enabled and if token response account decryption options has TDE + // and then if id token response has required device keys + // DevicePublicKey(UserKey) + // UserKey(DevicePublicKey) + // DeviceKey(DevicePrivateKey) + + // Once we have device keys coming back on id token response we can use this code + // const userKey = await this.deviceCryptoService.decryptUserKey( + // encryptedDevicePrivateKey, + // encryptedUserKey + // ); + // await this.cryptoService.setUserKey(userKey); + + // TODO: also admin approval request existence check should go here b/c that can give us a decrypted user key to set + // TODO: future passkey login strategy will need to support setting user key (decrypting via TDE or admin approval request) + // so might be worth moving this logic to a common place (base login strategy or a separate service?) + await this.cryptoService.setUserKeyMasterKey(tokenResponse.key); if (tokenResponse.keyConnectorUrl != null) { diff --git a/libs/common/src/enums/device-type.enum.ts b/libs/common/src/enums/device-type.enum.ts index d5ab33bbdd..663c482489 100644 --- a/libs/common/src/enums/device-type.enum.ts +++ b/libs/common/src/enums/device-type.enum.ts @@ -23,3 +23,16 @@ export enum DeviceType { SDK = 21, Server = 22, } + +export const MobileDeviceTypes: Set = new Set([ + DeviceType.Android, + DeviceType.iOS, + DeviceType.AndroidAmazon, +]); + +export const DesktopDeviceTypes: Set = new Set([ + DeviceType.WindowsDesktop, + DeviceType.MacOsDesktop, + DeviceType.LinuxDesktop, + DeviceType.UWP, +]); diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 2090c9a1cb..27672eddf6 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -263,8 +263,10 @@ export abstract class StateService { setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise; getDeviceKey: (options?: StorageOptions) => Promise; setDeviceKey: (value: DeviceKey, options?: StorageOptions) => Promise; - getAcctDecryptionOptions: (options?: StorageOptions) => Promise; - setAcctDecryptionOptions: ( + getAccountDecryptionOptions: ( + options?: StorageOptions + ) => Promise; + setAccountDecryptionOptions: ( value: AccountDecryptionOptions, options?: StorageOptions ) => Promise; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 77a6126d7b..4c169b0e0a 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -294,6 +294,17 @@ export class AccountDecryptionOptions { } } + // TODO: these nice getters don't work because the Account object is not properly being deserialized out of + // JSON (the Account static fromJSON method is not running) so these getters don't exist on the + // account decryptions options object when pulled out of state. This is a bug that needs to be fixed later on + // get hasTrustedDeviceOption(): boolean { + // return this.trustedDeviceOption !== null && this.trustedDeviceOption !== undefined; + // } + + // get hasKeyConnectorOption(): boolean { + // return this.keyConnectorOption !== null && this.keyConnectorOption !== undefined; + // } + static fromResponse(response: UserDecryptionOptionsResponse): AccountDecryptionOptions { if (response == null) { return null; @@ -322,7 +333,21 @@ export class AccountDecryptionOptions { return null; } - return Object.assign(new AccountDecryptionOptions(), obj); + const accountDecryptionOptions = Object.assign(new AccountDecryptionOptions(), obj); + + if (obj.trustedDeviceOption) { + accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption( + obj.trustedDeviceOption.hasAdminApproval + ); + } + + if (obj.keyConnectorOption) { + accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption( + obj.keyConnectorOption.keyConnectorUrl + ); + } + + return accountDecryptionOptions; } } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index d4482ea579..5b26f313e9 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -1321,7 +1321,7 @@ export class StateService< await this.saveAccount(account, options); } - async getAcctDecryptionOptions( + async getAccountDecryptionOptions( options?: StorageOptions ): Promise { options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); @@ -1335,7 +1335,7 @@ export class StateService< return account?.decryptionOptions as AccountDecryptionOptions; } - async setAcctDecryptionOptions( + async setAccountDecryptionOptions( value: AccountDecryptionOptions, options?: StorageOptions ): Promise { diff --git a/libs/common/src/services/device-crypto.service.implementation.ts b/libs/common/src/services/device-crypto.service.implementation.ts index 8e9baf015e..c415b976ac 100644 --- a/libs/common/src/services/device-crypto.service.implementation.ts +++ b/libs/common/src/services/device-crypto.service.implementation.ts @@ -86,4 +86,25 @@ export class DeviceCryptoService implements DeviceCryptoServiceAbstraction { return deviceKey; } + + // TODO: add proper types to parameters once we have them coming down from server + async decryptUserKey(encryptedDevicePrivateKey: any, encryptedUserKey: any): Promise { + // get device key + const existingDeviceKey = await this.stateService.getDeviceKey(); + + if (!existingDeviceKey) { + // TODO: not sure what to do here + // User doesn't have a device key anymore so device is untrusted + return; + } + + // attempt to decrypt encryptedDevicePrivateKey with device key + const devicePrivateKey = await this.encryptService.decryptToBytes( + encryptedDevicePrivateKey, + existingDeviceKey + ); + // Attempt to decrypt encryptedUserDataKey with devicePrivateKey + const userKey = await this.cryptoService.rsaDecrypt(encryptedUserKey, devicePrivateKey); + return new SymmetricCryptoKey(userKey) as UserKey; + } } diff --git a/libs/common/src/services/devices/devices-api.service.implementation.ts b/libs/common/src/services/devices/devices-api.service.implementation.ts index b037f386d3..a42c786cfa 100644 --- a/libs/common/src/services/devices/devices-api.service.implementation.ts +++ b/libs/common/src/services/devices/devices-api.service.implementation.ts @@ -1,5 +1,6 @@ import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction"; import { DeviceResponse } from "../../abstractions/devices/responses/device.response"; +import { DeviceType } from "../../enums"; import { ListResponse } from "../../models/response/list.response"; import { Utils } from "../../platform/misc/utils"; import { ApiService } from "../api.service"; @@ -45,6 +46,18 @@ export class DevicesApiServiceImplementation implements DevicesApiServiceAbstrac return new ListResponse(r, DeviceResponse); } + async getDevicesExistenceByTypes(deviceTypes: DeviceType[]): Promise { + const r = await this.apiService.send( + "POST", + "/devices/exist-by-types", + deviceTypes, + true, + true, + null + ); + return Boolean(r); + } + async updateTrustedDeviceKeys( deviceIdentifier: string, devicePublicKeyEncryptedUserKey: string, diff --git a/libs/common/src/services/devices/devices.service.implementation.ts b/libs/common/src/services/devices/devices.service.implementation.ts new file mode 100644 index 0000000000..23ab5fa390 --- /dev/null +++ b/libs/common/src/services/devices/devices.service.implementation.ts @@ -0,0 +1,76 @@ +import { Observable, defer, map } from "rxjs"; + +import { DevicesApiServiceAbstraction } from "../../abstractions/devices/devices-api.service.abstraction"; +import { DevicesServiceAbstraction } from "../../abstractions/devices/devices.service.abstraction"; +import { DeviceResponse } from "../../abstractions/devices/responses/device.response"; +import { DeviceView } from "../../abstractions/devices/views/device.view"; +import { DeviceType } from "../../enums"; +import { ListResponse } from "../../models/response/list.response"; + +/** + * @class DevicesServiceImplementation + * @implements {DevicesServiceAbstraction} + * @description Observable based data store service for Devices. + * note: defer is used to convert the promises to observables and to ensure + * that observables are created for each subscription + * (i.e., promsise --> observables are cold until subscribed to) + */ +export class DevicesServiceImplementation implements DevicesServiceAbstraction { + constructor(private devicesApiService: DevicesApiServiceAbstraction) {} + + /** + * @description Gets the list of all devices. + */ + getDevices$(): Observable> { + return defer(() => this.devicesApiService.getDevices()).pipe( + map((deviceResponses: ListResponse) => { + return deviceResponses.data.map((deviceResponse: DeviceResponse) => { + return new DeviceView(deviceResponse); + }); + }) + ); + } + + /** + * @description Returns whether the user has any devices of the specified types. + */ + getDevicesExistenceByTypes$(deviceTypes: DeviceType[]): Observable { + return defer(() => this.devicesApiService.getDevicesExistenceByTypes(deviceTypes)); + } + + /** + * @description Gets the device with the specified identifier. + */ + getDeviceByIdentifier$(deviceIdentifier: string): Observable { + return defer(() => this.devicesApiService.getDeviceByIdentifier(deviceIdentifier)).pipe( + map((deviceResponse: DeviceResponse) => new DeviceView(deviceResponse)) + ); + } + + /** + * @description Checks if a device is known for a user by user's email and device's identifier. + */ + isDeviceKnownForUser$(email: string, deviceIdentifier: string): Observable { + return defer(() => this.devicesApiService.getKnownDevice(email, deviceIdentifier)); + } + + /** + * @description Updates the keys for the specified device. + */ + + updateTrustedDeviceKeys$( + deviceIdentifier: string, + devicePublicKeyEncryptedUserKey: string, + userKeyEncryptedDevicePublicKey: string, + deviceKeyEncryptedDevicePrivateKey: string + ): Observable { + return defer(() => + this.devicesApiService.updateTrustedDeviceKeys( + deviceIdentifier, + devicePublicKeyEncryptedUserKey, + userKeyEncryptedDevicePublicKey, + deviceKeyEncryptedDevicePrivateKey + ) + ).pipe(map((deviceResponse: DeviceResponse) => new DeviceView(deviceResponse))); + } +}