@@ -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)));
+ }
+}