1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-03-11 13:30:39 +01:00

Feature/PM-1049 - TDEFflow 3 login decryption options - PR feedback changes (#5642)

* PM-1049 - PR Feedback change - Browser - replace incorrect use of routerlink with manual attribute styling to keep anchor styling + tab focus while not having a router action race condition for the log out action to complete.

* PM-1049 - PR Feedback - State Service changes - rename get/setAcctDecryptionOptions to  get/setAccountDecryptionOptions

* PM-1049 - PR Feedback changes - LoginDecryptionOptionsComp - Remove unncessary appA11yTitle directives as title / aria text would be identical to the displayed inner button text.

* DeviceType - Create sets of device types which other components can reference to avoid having to manually define groups of device types.

* PM-1049 - PR Feedback Changes - Update base-login-decryption-options component to leverage async piped observables per best practices. Updated all client templates to leverage new data streams.

* PM-1049 - BaseLoginDecryptionOptionsComp - Add validation service for generic error handling

* PM-1049 - DeviceResponse mistakenly had name as a number instead of a string

* PM-1049 - First draft of creating observable based data store service for Devices so that the base login comp can leverage it instead of calling the devices API service directly (as it will be moved into the SDK in the future).

* PM-1049 - Register new DevicesService on jslib-services module for use in components.

* PM-1049 - Add new hasDevicesOfTypes call to devices data store svc + devices API service.

* PM-1049 - BaseLoginDecryptionOptionsComp - wire up call to devicesService.hasDevicesOfTypes to replace getDevices() to avoid bringing down all trusted device information unnecessarily.

* PM-1049 - LoginDecryptionOptionsComp - Web HTML - clean up loading state so it displays spinner centered properly.

* PM-1049 - LoginDecryptionOptionsComp - Desktop HTML - Don't show login initiated title while page is loading to match other clients behavior.

* PM-1049 - Devices Services - Update naming of hasDevicesOfTypes to match new name on back end + route change to getDevicesExistenseByTypes

* PM-1049 - Device Response & View models - remove keys which are going to be deprecated on the base model

* PM-1049 - DevicesService - devicesBSubject --> devicesSubject rename per PR feedback

* PM-1049 - Devices Services - correct spelling of existence (*facepalm*)

* PM-1049 - Update comment for clarity per PR feedback

* PM-1049 - DevicesSvc - UserSymKey --> UserKey rename

* PM-1049 - BaseLoginDecryptionOptions - replace user email source - get from stateService vs tokenService.

* PM-1049 - BaseLoginDecryptionOptions - Remove uncessary check for userEmail as we will always have it here otherwise everything in the app is broken.

* PM-1049 - BaseLoginDecryptionOptions - Finish cleaning up removal of user email from showReqAdminApprovalBtn$ stream

* PM-1049 - LoginDecryptionOptionsComp - HTML revisions in web & browser to better space out buttons using tailwind or top margin to avoid need for multiple async pipes and shareReplay.

* PM-1049 - DevicesService - of course all observables should have $ suffix. Facepalm.

* PM-1049 - BaseLoginDecryptionOptionsComp - Update verbiage and style of destroy observable used for hooking into ngOnDestroy lifecycle to clean up all observables

* PM-1049 - BaseLoginDecryptionOptions - PR feedback changes - refactor user email to have an underlying bSubject stream to ensure subscription/promise execution separately from the template async pipe subscribing to the stream.

* PM-1049 - DevicesApiService - getDevicesExistenceByTypes - PR feedback - explicitly convert result to boolean instead of casting.

* PM-1049 - BaseLoginDecryptionOptionsComp - Add ShareReplay for getAccountDecryptionOptions + context per PR feedback

* PM-1049 - LoginDecryptionOptionsComp - Completely back away from template async pipe reactive approach as it caused massively increased complexity for little gain. Instead, just focus on reactively pulling asynchronously retrieved data and setting page loading state simply. This just works and is so much less overhead. + Add comments re flows of the component to be done later

* PM-1049- Revert DevicesService implementation from smart data store cache service giant mess into simple, clean data passthrough service to avoid complexity and keep moving forward. YAGNI

Co-authored-by: Andreas Coroiu <andreas@andreascoroiu.com>

* PM-1049 -  DeviceCryptoService - Add decryptUserKey method (WIP)

* PM-1049 - AccountDecryptionOptions - add get helpers for checking for trusted device / key connector decryption option existence.

* PM-1049 - SSO Login Strategy - added comments in setUserKey method for where we will probably be consuming device keys and determining if the device is trusted or not (i.e., if we can get a decrypted user sym key in memory)

* PM-1049 - DeviceCryptoSvc.decryptUserKey - Update method to properly use state service device key retrieval + add TODO to figure out what to do if user has previously had a device key and has cleared their local cache (which will result in the device being untrusted now)

* PM-1049 - SSO Login Strategy - add comment re future passkey login strategy support

* PM-2759 - SSO & 2FA components updated with v0 of navigation logic to send users to LoginDecryptionOptions

* PM-1049 - Account > AccountDecryptionOptions - can't create getter helper methods for determining if user has decryption options b/c of issues w/ account deserialization. Moving past b/c I can just easily check if the given options are not undefined.

* PM-2759 - Add TODOs for deprecation of id token response resetMasterPassword logic and replacement with use of accountDecryptionOptions

---------

Co-authored-by: Andreas Coroiu <andreas@andreascoroiu.com>
This commit is contained in:
Jared Snider 2023-06-27 19:58:59 -04:00 committed by GitHub
parent 9ff4bbbbe0
commit fa11b60c5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 451 additions and 134 deletions

View File

@ -41,9 +41,6 @@
(click)="approveFromOtherDevice()"
type="button"
class="btn primary block"
[ngClass]="{
'btn-bottom-margin': showReqAdminApprovalBtn || showApproveWithMasterPasswordBtn
}"
>
<b>{{ "approveFromYourOtherDevice" | i18n }}</b>
</button>
@ -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 }}
</button>
<button
type="button"
class="btn block"
class="btn block btn-top-margin"
*ngIf="showApproveWithMasterPasswordBtn"
(click)="approveWithMasterPassword()"
>
@ -70,7 +66,9 @@
<div class="small mx-5px">
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ userEmail }}</p>
<a [routerLink]="[]" (click)="logOut()">{{ "notYou" | i18n }}</a>
<a tabindex="0" role="button" style="cursor: pointer" (click)="logOut()">{{
"notYou" | i18n
}}</a>
</div>
</ng-container>
</div>

View File

@ -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: "browser-login-decryption-options",
@ -16,21 +16,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
);
}
}

View File

@ -6,6 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
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";
@ -35,6 +36,7 @@ export class SsoComponent extends BaseSsoComponent {
syncService: SyncService,
environmentService: EnvironmentService,
logService: LogService,
configService: ConfigServiceAbstraction,
private vaultTimeoutService: VaultTimeoutService
) {
super(
@ -48,7 +50,8 @@ export class SsoComponent extends BaseSsoComponent {
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService
logService,
configService
);
const url = this.environmentService.getWebVaultUrl();

View File

@ -11,6 +11,7 @@ import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.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";
@ -48,6 +49,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
twoFactorService: TwoFactorService,
appIdService: AppIdService,
loginService: LoginService,
configService: ConfigServiceAbstraction,
private dialogService: DialogServiceAbstraction
) {
super(
@ -63,7 +65,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
logService,
twoFactorService,
appIdService,
loginService
loginService,
configService
);
super.onSuccessfulLogin = () => {
this.loginService.clearValues();

View File

@ -671,8 +671,8 @@ main {
margin-right: 5px;
}
.btn-bottom-margin {
margin-bottom: 12px;
.btn-top-margin {
margin-top: 12px;
}
#rememberThisDeviceHintText {

View File

@ -1,13 +1,13 @@
<div id="login-decryption-options-page">
<div id="content" class="content">
<img class="logo-image" alt="Bitwarden" />
<h1 id="heading">{{ "logInInitiated" | i18n }}</h1>
<div class="container loading-spinner" *ngIf="loading">
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
</div>
<ng-container *ngIf="!loading">
<h1 id="heading">{{ "logInInitiated" | i18n }}</h1>
<h6 id="subHeading" class="standard-bottom-margin">{{ "deviceApprovalRequired" | i18n }}</h6>
<form id="rememberDeviceForm" class="standard-bottom-margin" [formGroup]="rememberDeviceForm">

View File

@ -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: "desktop-login-decryption-options",
@ -16,21 +16,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
);
}
}

View File

@ -4,6 +4,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
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";
@ -30,7 +31,8 @@ export class SsoComponent extends BaseSsoComponent {
cryptoFunctionService: CryptoFunctionService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
logService: LogService
logService: LogService,
configService: ConfigServiceAbstraction
) {
super(
authService,
@ -43,7 +45,8 @@ export class SsoComponent extends BaseSsoComponent {
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService
logService,
configService
);
super.onSuccessfulLogin = () => {
return syncService.fullSync(true);

View File

@ -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";
@ -43,7 +44,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
logService: LogService,
twoFactorService: TwoFactorService,
appIdService: AppIdService,
loginService: LoginService
loginService: LoginService,
configService: ConfigServiceAbstraction
) {
super(
authService,
@ -58,7 +60,8 @@ export class TwoFactorComponent extends BaseTwoFactorComponent {
logService,
twoFactorService,
appIdService,
loginService
loginService,
configService
);
super.onSuccessfulLogin = () => {
this.loginService.clearValues();

View File

@ -1,13 +1,4 @@
<ng-container *ngIf="loading">
<i
class="bwi bwi-spinner bwi-spin text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</ng-container>
<div class="tw-container tw-mx-auto" *ngIf="!loading">
<div class="tw-container tw-mx-auto">
<div
class="tw-mx-auto tw-mt-5 tw-flex tw-max-w-lg tw-flex-col tw-items-center tw-justify-center tw-p-8"
>
@ -15,7 +6,19 @@
<img class="logo logo-themed" alt="Bitwarden" />
</div>
<ng-container *ngIf="loading">
<p class="text-center">
<i
class="bwi bwi-spinner bwi-spin bwi-2x text-muted"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "loading" | i18n }}</span>
</p>
</ng-container>
<div
*ngIf="!loading"
class="tw-w-full tw-rounded-md tw-border tw-border-solid tw-border-secondary-300 tw-bg-background tw-p-6"
>
<h2 bitTypography="h2" class="tw-mb-6">{{ "loginInitiated" | i18n }}</h2>
@ -30,7 +33,7 @@
</bit-form-control>
</form>
<div class="tw-mb-6 tw-flex tw-flex-col">
<div class="tw-mb-6 tw-flex tw-flex-col tw-space-y-3">
<button
*ngIf="showApproveFromOtherDeviceBtn"
(click)="approveFromOtherDevice()"
@ -38,8 +41,6 @@
type="button"
buttonType="primary"
[block]="true"
[ngClass]="{ 'tw-mb-3': showReqAdminApprovalBtn || showApproveWithMasterPasswordBtn }"
[appA11yTitle]="'approveFromYourOtherDevice' | i18n"
>
{{ "approveFromYourOtherDevice" | i18n }}
</button>
@ -50,8 +51,6 @@
bitButton
type="button"
buttonType="secondary"
[ngClass]="{ 'tw-mb-3': showApproveWithMasterPasswordBtn }"
[appA11yTitle]="'requestAdminApproval' | i18n"
>
{{ "requestAdminApproval" | i18n }}
</button>
@ -62,7 +61,6 @@
bitButton
type="button"
buttonType="secondary"
[appA11yTitle]="'approveWithMasterPassword' | i18n"
>
{{ "approveWithMasterPassword" | i18n }}
</button>

View File

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

View File

@ -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";

View File

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

View File

@ -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";

View File

@ -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<void> = new Subject();
private destroy$ = new Subject<void>();
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<AccountDecryptionOptions> = 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();
}
}

View File

@ -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) {

View File

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

View File

@ -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,

View File

@ -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<DeviceResponse>;
getDeviceKey: () => Promise<DeviceKey>;
// TODO: update param types when available
decryptUserKey: (encryptedDevicePrivateKey: any, encryptedUserKey: any) => Promise<UserKey>;
}

View File

@ -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<DeviceResponse>;
getDevices: () => Promise<ListResponse<DeviceResponse>>;
getDevicesExistenceByTypes: (deviceTypes: DeviceType[]) => Promise<boolean>;
updateTrustedDeviceKeys: (
deviceIdentifier: string,

View File

@ -0,0 +1,18 @@
import { Observable } from "rxjs";
import { DeviceType } from "../../enums";
import { DeviceView } from "./views/device.view";
export abstract class DevicesServiceAbstraction {
getDevices$: () => Observable<Array<DeviceView>>;
getDevicesExistenceByTypes$: (deviceTypes: DeviceType[]) => Observable<boolean>;
getDeviceByIdentifier$: (deviceIdentifier: string) => Observable<DeviceView>;
isDeviceKnownForUser$: (email: string, deviceIdentifier: string) => Observable<boolean>;
updateTrustedDeviceKeys$: (
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,
userKeyEncryptedDevicePublicKey: string,
deviceKeyEncryptedDevicePrivateKey: string
) => Observable<DeviceView>;
}

View File

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

View File

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

View File

@ -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) {

View File

@ -23,3 +23,16 @@ export enum DeviceType {
SDK = 21,
Server = 22,
}
export const MobileDeviceTypes: Set<DeviceType> = new Set([
DeviceType.Android,
DeviceType.iOS,
DeviceType.AndroidAmazon,
]);
export const DesktopDeviceTypes: Set<DeviceType> = new Set([
DeviceType.WindowsDesktop,
DeviceType.MacOsDesktop,
DeviceType.LinuxDesktop,
DeviceType.UWP,
]);

View File

@ -263,8 +263,10 @@ export abstract class StateService<T extends Account = Account> {
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
getDeviceKey: (options?: StorageOptions) => Promise<DeviceKey | null>;
setDeviceKey: (value: DeviceKey, options?: StorageOptions) => Promise<void>;
getAcctDecryptionOptions: (options?: StorageOptions) => Promise<AccountDecryptionOptions | null>;
setAcctDecryptionOptions: (
getAccountDecryptionOptions: (
options?: StorageOptions
) => Promise<AccountDecryptionOptions | null>;
setAccountDecryptionOptions: (
value: AccountDecryptionOptions,
options?: StorageOptions
) => Promise<void>;

View File

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

View File

@ -1321,7 +1321,7 @@ export class StateService<
await this.saveAccount(account, options);
}
async getAcctDecryptionOptions(
async getAccountDecryptionOptions(
options?: StorageOptions
): Promise<AccountDecryptionOptions | null> {
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<void> {

View File

@ -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<UserKey> {
// 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;
}
}

View File

@ -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<boolean> {
const r = await this.apiService.send(
"POST",
"/devices/exist-by-types",
deviceTypes,
true,
true,
null
);
return Boolean(r);
}
async updateTrustedDeviceKeys(
deviceIdentifier: string,
devicePublicKeyEncryptedUserKey: string,

View File

@ -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<Array<DeviceView>> {
return defer(() => this.devicesApiService.getDevices()).pipe(
map((deviceResponses: ListResponse<DeviceResponse>) => {
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<boolean> {
return defer(() => this.devicesApiService.getDevicesExistenceByTypes(deviceTypes));
}
/**
* @description Gets the device with the specified identifier.
*/
getDeviceByIdentifier$(deviceIdentifier: string): Observable<DeviceView> {
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<boolean> {
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<DeviceView> {
return defer(() =>
this.devicesApiService.updateTrustedDeviceKeys(
deviceIdentifier,
devicePublicKeyEncryptedUserKey,
userKeyEncryptedDevicePublicKey,
deviceKeyEncryptedDevicePrivateKey
)
).pipe(map((deviceResponse: DeviceResponse) => new DeviceView(deviceResponse)));
}
}