Merge branch 'main' into auth/pm-7392/token-service-add-secure-storage-fallback
This commit is contained in:
commit
5791be9143
|
@ -3044,6 +3044,12 @@
|
||||||
"accountSecurity": {
|
"accountSecurity": {
|
||||||
"message": "Account security"
|
"message": "Account security"
|
||||||
},
|
},
|
||||||
|
"notifications": {
|
||||||
|
"message": "Notifications"
|
||||||
|
},
|
||||||
|
"appearance": {
|
||||||
|
"message": "Appearance"
|
||||||
|
},
|
||||||
"errorAssigningTargetCollection": {
|
"errorAssigningTargetCollection": {
|
||||||
"message": "Error assigning target collection."
|
"message": "Error assigning target collection."
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,20 +4,35 @@ import {
|
||||||
} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
|
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
encryptServiceFactory,
|
||||||
|
EncryptServiceInitOptions,
|
||||||
|
} from "../../../platform/background/service-factories/encrypt-service.factory";
|
||||||
import {
|
import {
|
||||||
CachedServices,
|
CachedServices,
|
||||||
factory,
|
factory,
|
||||||
FactoryOptions,
|
FactoryOptions,
|
||||||
} from "../../../platform/background/service-factories/factory-options";
|
} from "../../../platform/background/service-factories/factory-options";
|
||||||
|
import {
|
||||||
|
keyGenerationServiceFactory,
|
||||||
|
KeyGenerationServiceInitOptions,
|
||||||
|
} from "../../../platform/background/service-factories/key-generation-service.factory";
|
||||||
import {
|
import {
|
||||||
stateProviderFactory,
|
stateProviderFactory,
|
||||||
StateProviderInitOptions,
|
StateProviderInitOptions,
|
||||||
} from "../../../platform/background/service-factories/state-provider.factory";
|
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||||
|
import {
|
||||||
|
stateServiceFactory,
|
||||||
|
StateServiceInitOptions,
|
||||||
|
} from "../../../platform/background/service-factories/state-service.factory";
|
||||||
|
|
||||||
type MasterPasswordServiceFactoryOptions = FactoryOptions;
|
type MasterPasswordServiceFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions &
|
export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions &
|
||||||
StateProviderInitOptions;
|
StateProviderInitOptions &
|
||||||
|
StateServiceInitOptions &
|
||||||
|
KeyGenerationServiceInitOptions &
|
||||||
|
EncryptServiceInitOptions;
|
||||||
|
|
||||||
export function internalMasterPasswordServiceFactory(
|
export function internalMasterPasswordServiceFactory(
|
||||||
cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices,
|
cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices,
|
||||||
|
@ -27,7 +42,13 @@ export function internalMasterPasswordServiceFactory(
|
||||||
cache,
|
cache,
|
||||||
"masterPasswordService",
|
"masterPasswordService",
|
||||||
opts,
|
opts,
|
||||||
async () => new MasterPasswordService(await stateProviderFactory(cache, opts)),
|
async () =>
|
||||||
|
new MasterPasswordService(
|
||||||
|
await stateProviderFactory(cache, opts),
|
||||||
|
await stateServiceFactory(cache, opts),
|
||||||
|
await keyGenerationServiceFactory(cache, opts),
|
||||||
|
await encryptServiceFactory(cache, opts),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
import { PinCryptoServiceAbstraction, PinCryptoService } from "@bitwarden/auth/common";
|
|
||||||
|
|
||||||
import {
|
|
||||||
VaultTimeoutSettingsServiceInitOptions,
|
|
||||||
vaultTimeoutSettingsServiceFactory,
|
|
||||||
} from "../../../background/service-factories/vault-timeout-settings-service.factory";
|
|
||||||
import {
|
|
||||||
CryptoServiceInitOptions,
|
|
||||||
cryptoServiceFactory,
|
|
||||||
} from "../../../platform/background/service-factories/crypto-service.factory";
|
|
||||||
import {
|
|
||||||
FactoryOptions,
|
|
||||||
CachedServices,
|
|
||||||
factory,
|
|
||||||
} from "../../../platform/background/service-factories/factory-options";
|
|
||||||
import {
|
|
||||||
LogServiceInitOptions,
|
|
||||||
logServiceFactory,
|
|
||||||
} from "../../../platform/background/service-factories/log-service.factory";
|
|
||||||
import {
|
|
||||||
StateServiceInitOptions,
|
|
||||||
stateServiceFactory,
|
|
||||||
} from "../../../platform/background/service-factories/state-service.factory";
|
|
||||||
|
|
||||||
import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory";
|
|
||||||
|
|
||||||
type PinCryptoServiceFactoryOptions = FactoryOptions;
|
|
||||||
|
|
||||||
export type PinCryptoServiceInitOptions = PinCryptoServiceFactoryOptions &
|
|
||||||
StateServiceInitOptions &
|
|
||||||
CryptoServiceInitOptions &
|
|
||||||
VaultTimeoutSettingsServiceInitOptions &
|
|
||||||
LogServiceInitOptions &
|
|
||||||
KdfConfigServiceInitOptions;
|
|
||||||
|
|
||||||
export function pinCryptoServiceFactory(
|
|
||||||
cache: { pinCryptoService?: PinCryptoServiceAbstraction } & CachedServices,
|
|
||||||
opts: PinCryptoServiceInitOptions,
|
|
||||||
): Promise<PinCryptoServiceAbstraction> {
|
|
||||||
return factory(
|
|
||||||
cache,
|
|
||||||
"pinCryptoService",
|
|
||||||
opts,
|
|
||||||
async () =>
|
|
||||||
new PinCryptoService(
|
|
||||||
await stateServiceFactory(cache, opts),
|
|
||||||
await cryptoServiceFactory(cache, opts),
|
|
||||||
await vaultTimeoutSettingsServiceFactory(cache, opts),
|
|
||||||
await logServiceFactory(cache, opts),
|
|
||||||
await kdfConfigServiceFactory(cache, opts),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { PinServiceAbstraction, PinService } from "@bitwarden/auth/common";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CryptoFunctionServiceInitOptions,
|
||||||
|
cryptoFunctionServiceFactory,
|
||||||
|
} from "../../../platform/background/service-factories/crypto-function-service.factory";
|
||||||
|
import {
|
||||||
|
EncryptServiceInitOptions,
|
||||||
|
encryptServiceFactory,
|
||||||
|
} from "../../../platform/background/service-factories/encrypt-service.factory";
|
||||||
|
import {
|
||||||
|
FactoryOptions,
|
||||||
|
CachedServices,
|
||||||
|
factory,
|
||||||
|
} from "../../../platform/background/service-factories/factory-options";
|
||||||
|
import {
|
||||||
|
KeyGenerationServiceInitOptions,
|
||||||
|
keyGenerationServiceFactory,
|
||||||
|
} from "../../../platform/background/service-factories/key-generation-service.factory";
|
||||||
|
import {
|
||||||
|
LogServiceInitOptions,
|
||||||
|
logServiceFactory,
|
||||||
|
} from "../../../platform/background/service-factories/log-service.factory";
|
||||||
|
import {
|
||||||
|
StateProviderInitOptions,
|
||||||
|
stateProviderFactory,
|
||||||
|
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||||
|
import {
|
||||||
|
StateServiceInitOptions,
|
||||||
|
stateServiceFactory,
|
||||||
|
} from "../../../platform/background/service-factories/state-service.factory";
|
||||||
|
|
||||||
|
import { AccountServiceInitOptions, accountServiceFactory } from "./account-service.factory";
|
||||||
|
import { KdfConfigServiceInitOptions, kdfConfigServiceFactory } from "./kdf-config-service.factory";
|
||||||
|
import {
|
||||||
|
MasterPasswordServiceInitOptions,
|
||||||
|
masterPasswordServiceFactory,
|
||||||
|
} from "./master-password-service.factory";
|
||||||
|
|
||||||
|
type PinServiceFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
|
export type PinServiceInitOptions = PinServiceFactoryOptions &
|
||||||
|
AccountServiceInitOptions &
|
||||||
|
CryptoFunctionServiceInitOptions &
|
||||||
|
EncryptServiceInitOptions &
|
||||||
|
KdfConfigServiceInitOptions &
|
||||||
|
KeyGenerationServiceInitOptions &
|
||||||
|
LogServiceInitOptions &
|
||||||
|
MasterPasswordServiceInitOptions &
|
||||||
|
StateProviderInitOptions &
|
||||||
|
StateServiceInitOptions;
|
||||||
|
|
||||||
|
export function pinServiceFactory(
|
||||||
|
cache: { pinService?: PinServiceAbstraction } & CachedServices,
|
||||||
|
opts: PinServiceInitOptions,
|
||||||
|
): Promise<PinServiceAbstraction> {
|
||||||
|
return factory(
|
||||||
|
cache,
|
||||||
|
"pinService",
|
||||||
|
opts,
|
||||||
|
async () =>
|
||||||
|
new PinService(
|
||||||
|
await accountServiceFactory(cache, opts),
|
||||||
|
await cryptoFunctionServiceFactory(cache, opts),
|
||||||
|
await encryptServiceFactory(cache, opts),
|
||||||
|
await kdfConfigServiceFactory(cache, opts),
|
||||||
|
await keyGenerationServiceFactory(cache, opts),
|
||||||
|
await logServiceFactory(cache, opts),
|
||||||
|
await masterPasswordServiceFactory(cache, opts),
|
||||||
|
await stateProviderFactory(cache, opts),
|
||||||
|
await stateServiceFactory(cache, opts),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -37,7 +37,7 @@ import {
|
||||||
internalMasterPasswordServiceFactory,
|
internalMasterPasswordServiceFactory,
|
||||||
MasterPasswordServiceInitOptions,
|
MasterPasswordServiceInitOptions,
|
||||||
} from "./master-password-service.factory";
|
} from "./master-password-service.factory";
|
||||||
import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory";
|
import { PinServiceInitOptions, pinServiceFactory } from "./pin-service.factory";
|
||||||
import {
|
import {
|
||||||
userDecryptionOptionsServiceFactory,
|
userDecryptionOptionsServiceFactory,
|
||||||
UserDecryptionOptionsServiceInitOptions,
|
UserDecryptionOptionsServiceInitOptions,
|
||||||
|
@ -57,7 +57,7 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
|
||||||
I18nServiceInitOptions &
|
I18nServiceInitOptions &
|
||||||
UserVerificationApiServiceInitOptions &
|
UserVerificationApiServiceInitOptions &
|
||||||
UserDecryptionOptionsServiceInitOptions &
|
UserDecryptionOptionsServiceInitOptions &
|
||||||
PinCryptoServiceInitOptions &
|
PinServiceInitOptions &
|
||||||
LogServiceInitOptions &
|
LogServiceInitOptions &
|
||||||
VaultTimeoutSettingsServiceInitOptions &
|
VaultTimeoutSettingsServiceInitOptions &
|
||||||
PlatformUtilsServiceInitOptions &
|
PlatformUtilsServiceInitOptions &
|
||||||
|
@ -80,7 +80,7 @@ export function userVerificationServiceFactory(
|
||||||
await i18nServiceFactory(cache, opts),
|
await i18nServiceFactory(cache, opts),
|
||||||
await userVerificationApiServiceFactory(cache, opts),
|
await userVerificationApiServiceFactory(cache, opts),
|
||||||
await userDecryptionOptionsServiceFactory(cache, opts),
|
await userDecryptionOptionsServiceFactory(cache, opts),
|
||||||
await pinCryptoServiceFactory(cache, opts),
|
await pinServiceFactory(cache, opts),
|
||||||
await logServiceFactory(cache, opts),
|
await logServiceFactory(cache, opts),
|
||||||
await vaultTimeoutSettingsServiceFactory(cache, opts),
|
await vaultTimeoutSettingsServiceFactory(cache, opts),
|
||||||
await platformUtilsServiceFactory(cache, opts),
|
await platformUtilsServiceFactory(cache, opts),
|
||||||
|
|
|
@ -12,8 +12,16 @@
|
||||||
<input class="tw-font-mono" bitInput type="password" formControlName="pin" />
|
<input class="tw-font-mono" bitInput type="password" formControlName="pin" />
|
||||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<label class="tw-flex tw-items-start tw-gap-2" *ngIf="showMasterPassOnRestart">
|
<label
|
||||||
<input class="tw-mt-1" type="checkbox" bitCheckbox formControlName="masterPassOnRestart" />
|
class="tw-flex tw-items-start tw-gap-2"
|
||||||
|
*ngIf="showMasterPasswordOnClientRestartOption"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="tw-mt-1"
|
||||||
|
type="checkbox"
|
||||||
|
bitCheckbox
|
||||||
|
formControlName="requireMasterPasswordOnClientRestart"
|
||||||
|
/>
|
||||||
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
|
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Router } from "@angular/router";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
||||||
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
|
@ -63,7 +63,7 @@ export class LockComponent extends BaseLockComponent {
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
deviceTrustService: DeviceTrustServiceAbstraction,
|
deviceTrustService: DeviceTrustServiceAbstraction,
|
||||||
userVerificationService: UserVerificationService,
|
userVerificationService: UserVerificationService,
|
||||||
pinCryptoService: PinCryptoServiceAbstraction,
|
pinService: PinServiceAbstraction,
|
||||||
private routerService: BrowserRouterService,
|
private routerService: BrowserRouterService,
|
||||||
biometricStateService: BiometricStateService,
|
biometricStateService: BiometricStateService,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
|
@ -89,7 +89,7 @@ export class LockComponent extends BaseLockComponent {
|
||||||
dialogService,
|
dialogService,
|
||||||
deviceTrustService,
|
deviceTrustService,
|
||||||
userVerificationService,
|
userVerificationService,
|
||||||
pinCryptoService,
|
pinService,
|
||||||
biometricStateService,
|
biometricStateService,
|
||||||
accountService,
|
accountService,
|
||||||
authService,
|
authService,
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
|
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
|
||||||
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
@ -71,6 +72,7 @@ export class AccountSecurityComponent implements OnInit {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
|
private pinService: PinServiceAbstraction,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
@ -131,7 +133,6 @@ export class AccountSecurityComponent implements OnInit {
|
||||||
if (timeout === -2 && !showOnLocked) {
|
if (timeout === -2 && !showOnLocked) {
|
||||||
timeout = -1;
|
timeout = -1;
|
||||||
}
|
}
|
||||||
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
|
|
||||||
|
|
||||||
this.form.controls.vaultTimeout.valueChanges
|
this.form.controls.vaultTimeout.valueChanges
|
||||||
.pipe(
|
.pipe(
|
||||||
|
@ -153,12 +154,14 @@ export class AccountSecurityComponent implements OnInit {
|
||||||
)
|
)
|
||||||
.subscribe();
|
.subscribe();
|
||||||
|
|
||||||
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
vaultTimeout: timeout,
|
vaultTimeout: timeout,
|
||||||
vaultTimeoutAction: await firstValueFrom(
|
vaultTimeoutAction: await firstValueFrom(
|
||||||
this.vaultTimeoutSettingsService.vaultTimeoutAction$(),
|
this.vaultTimeoutSettingsService.vaultTimeoutAction$(),
|
||||||
),
|
),
|
||||||
pin: pinStatus !== "DISABLED",
|
pin: await this.pinService.isPinSet(userId),
|
||||||
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
||||||
enableAutoBiometricsPrompt: await firstValueFrom(
|
enableAutoBiometricsPrompt: await firstValueFrom(
|
||||||
this.biometricStateService.promptAutomatically$,
|
this.biometricStateService.promptAutomatically$,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<form #form (ngSubmit)="submit()">
|
<form #form (ngSubmit)="submit()">
|
||||||
<header>
|
<header>
|
||||||
<div class="left">
|
<div class="left">
|
||||||
<button type="button" routerLink="/tabs/settings">
|
<button type="button" routerLink="/notifications">
|
||||||
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||||
<span>{{ "back" | i18n }}</span>
|
<span>{{ "back" | i18n }}</span>
|
||||||
</button>
|
</button>
|
|
@ -8,8 +8,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
import { enableAccountSwitching } from "../../platform/flags";
|
import { enableAccountSwitching } from "../../../platform/flags";
|
||||||
|
|
||||||
interface ExcludedDomain {
|
interface ExcludedDomain {
|
||||||
uri: string;
|
uri: string;
|
|
@ -0,0 +1,89 @@
|
||||||
|
<header>
|
||||||
|
<div class="left">
|
||||||
|
<button type="button" routerLink="/tabs/settings">
|
||||||
|
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||||
|
<span>{{ "back" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h1 class="center">
|
||||||
|
<span class="title">{{ "notifications" | i18n }}</span>
|
||||||
|
</h1>
|
||||||
|
<div class="right">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main tabindex="-1">
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="use-passkeys">{{ "enableUsePasskeys" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="use-passkeys"
|
||||||
|
type="checkbox"
|
||||||
|
aria-describedby="use-passkeysHelp"
|
||||||
|
(change)="updateEnablePasskeys()"
|
||||||
|
[(ngModel)]="enablePasskeys"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="use-passkeysHelp" class="box-footer">
|
||||||
|
{{ "usePasskeysDesc" | i18n }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="addlogin-notification-bar">{{ "enableAddLoginNotification" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="addlogin-notification-bar"
|
||||||
|
type="checkbox"
|
||||||
|
aria-describedby="addlogin-notification-barHelp"
|
||||||
|
(change)="updateAddLoginNotification()"
|
||||||
|
[(ngModel)]="enableAddLoginNotification"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="addlogin-notification-barHelp" class="box-footer">
|
||||||
|
{{
|
||||||
|
accountSwitcherEnabled
|
||||||
|
? ("addLoginNotificationDescAlt" | i18n)
|
||||||
|
: ("addLoginNotificationDesc" | i18n)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="changedpass-notification-bar">{{
|
||||||
|
"enableChangedPasswordNotification" | i18n
|
||||||
|
}}</label>
|
||||||
|
<input
|
||||||
|
id="changedpass-notification-bar"
|
||||||
|
type="checkbox"
|
||||||
|
aria-describedby="changedpass-notification-barHelp"
|
||||||
|
(change)="updateChangedPasswordNotification()"
|
||||||
|
[(ngModel)]="enableChangedPasswordNotification"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="changedpass-notification-barHelp" class="box-footer">
|
||||||
|
{{
|
||||||
|
accountSwitcherEnabled
|
||||||
|
? ("changedPasswordNotificationDescAlt" | i18n)
|
||||||
|
: ("changedPasswordNotificationDesc" | i18n)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box list">
|
||||||
|
<div class="box-content single-line">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/excluded-domains"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "excludedDomains" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
|
|
||||||
|
import { enableAccountSwitching } from "../../../platform/flags";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "autofill-notification-settings",
|
||||||
|
templateUrl: "notifications.component.html",
|
||||||
|
})
|
||||||
|
export class NotifcationsSettingsComponent implements OnInit {
|
||||||
|
enableAddLoginNotification = false;
|
||||||
|
enableChangedPasswordNotification = false;
|
||||||
|
enablePasskeys = true;
|
||||||
|
accountSwitcherEnabled = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||||
|
private vaultSettingsService: VaultSettingsService,
|
||||||
|
) {
|
||||||
|
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.enableAddLoginNotification = await firstValueFrom(
|
||||||
|
this.userNotificationSettingsService.enableAddedLoginPrompt$,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.enableChangedPasswordNotification = await firstValueFrom(
|
||||||
|
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateAddLoginNotification() {
|
||||||
|
await this.userNotificationSettingsService.setEnableAddedLoginPrompt(
|
||||||
|
this.enableAddLoginNotification,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateChangedPasswordNotification() {
|
||||||
|
await this.userNotificationSettingsService.setEnableChangedPasswordPrompt(
|
||||||
|
this.enableChangedPasswordNotification,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEnablePasskeys() {
|
||||||
|
await this.vaultSettingsService.setEnablePasskeys(this.enablePasskeys);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs";
|
import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PinCryptoServiceAbstraction,
|
PinServiceAbstraction,
|
||||||
PinCryptoService,
|
PinService,
|
||||||
InternalUserDecryptionOptionsServiceAbstraction,
|
InternalUserDecryptionOptionsServiceAbstraction,
|
||||||
UserDecryptionOptionsService,
|
UserDecryptionOptionsService,
|
||||||
AuthRequestServiceAbstraction,
|
AuthRequestServiceAbstraction,
|
||||||
|
@ -319,7 +319,7 @@ export default class MainBackground {
|
||||||
authRequestService: AuthRequestServiceAbstraction;
|
authRequestService: AuthRequestServiceAbstraction;
|
||||||
accountService: AccountServiceAbstraction;
|
accountService: AccountServiceAbstraction;
|
||||||
globalStateProvider: GlobalStateProvider;
|
globalStateProvider: GlobalStateProvider;
|
||||||
pinCryptoService: PinCryptoServiceAbstraction;
|
pinService: PinServiceAbstraction;
|
||||||
singleUserStateProvider: SingleUserStateProvider;
|
singleUserStateProvider: SingleUserStateProvider;
|
||||||
activeUserStateProvider: ActiveUserStateProvider;
|
activeUserStateProvider: ActiveUserStateProvider;
|
||||||
derivedStateProvider: DerivedStateProvider;
|
derivedStateProvider: DerivedStateProvider;
|
||||||
|
@ -544,13 +544,31 @@ export default class MainBackground {
|
||||||
|
|
||||||
const themeStateService = new DefaultThemeStateService(this.globalStateProvider);
|
const themeStateService = new DefaultThemeStateService(this.globalStateProvider);
|
||||||
|
|
||||||
this.masterPasswordService = new MasterPasswordService(this.stateProvider);
|
this.masterPasswordService = new MasterPasswordService(
|
||||||
|
this.stateProvider,
|
||||||
|
this.stateService,
|
||||||
|
this.keyGenerationService,
|
||||||
|
this.encryptService,
|
||||||
|
);
|
||||||
|
|
||||||
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
||||||
|
|
||||||
this.kdfConfigService = new KdfConfigService(this.stateProvider);
|
this.kdfConfigService = new KdfConfigService(this.stateProvider);
|
||||||
|
|
||||||
|
this.pinService = new PinService(
|
||||||
|
this.accountService,
|
||||||
|
this.cryptoFunctionService,
|
||||||
|
this.encryptService,
|
||||||
|
this.kdfConfigService,
|
||||||
|
this.keyGenerationService,
|
||||||
|
this.logService,
|
||||||
|
this.masterPasswordService,
|
||||||
|
this.stateProvider,
|
||||||
|
this.stateService,
|
||||||
|
);
|
||||||
|
|
||||||
this.cryptoService = new BrowserCryptoService(
|
this.cryptoService = new BrowserCryptoService(
|
||||||
|
this.pinService,
|
||||||
this.masterPasswordService,
|
this.masterPasswordService,
|
||||||
this.keyGenerationService,
|
this.keyGenerationService,
|
||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
|
@ -705,6 +723,8 @@ export default class MainBackground {
|
||||||
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
|
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
|
||||||
|
|
||||||
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
||||||
|
this.accountService,
|
||||||
|
this.pinService,
|
||||||
this.userDecryptionOptionsService,
|
this.userDecryptionOptionsService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.tokenService,
|
this.tokenService,
|
||||||
|
@ -713,14 +733,6 @@ export default class MainBackground {
|
||||||
this.biometricStateService,
|
this.biometricStateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.pinCryptoService = new PinCryptoService(
|
|
||||||
this.stateService,
|
|
||||||
this.cryptoService,
|
|
||||||
this.vaultTimeoutSettingsService,
|
|
||||||
this.logService,
|
|
||||||
this.kdfConfigService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.userVerificationService = new UserVerificationService(
|
this.userVerificationService = new UserVerificationService(
|
||||||
this.stateService,
|
this.stateService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
|
@ -729,7 +741,7 @@ export default class MainBackground {
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.userVerificationApiService,
|
this.userVerificationApiService,
|
||||||
this.userDecryptionOptionsService,
|
this.userDecryptionOptionsService,
|
||||||
this.pinCryptoService,
|
this.pinService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.vaultTimeoutSettingsService,
|
this.vaultTimeoutSettingsService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
|
@ -851,11 +863,13 @@ export default class MainBackground {
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.collectionService,
|
this.collectionService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
|
this.pinService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.individualVaultExportService = new IndividualVaultExportService(
|
this.individualVaultExportService = new IndividualVaultExportService(
|
||||||
this.folderService,
|
this.folderService,
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
|
this.pinService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
this.kdfConfigService,
|
this.kdfConfigService,
|
||||||
|
@ -864,6 +878,7 @@ export default class MainBackground {
|
||||||
this.organizationVaultExportService = new OrganizationVaultExportService(
|
this.organizationVaultExportService = new OrganizationVaultExportService(
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
|
this.pinService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
this.collectionService,
|
this.collectionService,
|
||||||
|
@ -914,6 +929,7 @@ export default class MainBackground {
|
||||||
};
|
};
|
||||||
|
|
||||||
this.systemService = new SystemService(
|
this.systemService = new SystemService(
|
||||||
|
this.pinService,
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
systemUtilsServiceReloadCallback,
|
systemUtilsServiceReloadCallback,
|
||||||
|
|
|
@ -356,7 +356,7 @@ export class NativeMessagingBackground {
|
||||||
const masterKey = new SymmetricCryptoKey(
|
const masterKey = new SymmetricCryptoKey(
|
||||||
Utils.fromB64ToArray(message.keyB64),
|
Utils.fromB64ToArray(message.keyB64),
|
||||||
) as MasterKey;
|
) as MasterKey;
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||||
masterKey,
|
masterKey,
|
||||||
encUserKey,
|
encUserKey,
|
||||||
);
|
);
|
||||||
|
|
|
@ -5,6 +5,14 @@ import {
|
||||||
policyServiceFactory,
|
policyServiceFactory,
|
||||||
PolicyServiceInitOptions,
|
PolicyServiceInitOptions,
|
||||||
} from "../../admin-console/background/service-factories/policy-service.factory";
|
} from "../../admin-console/background/service-factories/policy-service.factory";
|
||||||
|
import {
|
||||||
|
accountServiceFactory,
|
||||||
|
AccountServiceInitOptions,
|
||||||
|
} from "../../auth/background/service-factories/account-service.factory";
|
||||||
|
import {
|
||||||
|
pinServiceFactory,
|
||||||
|
PinServiceInitOptions,
|
||||||
|
} from "../../auth/background/service-factories/pin-service.factory";
|
||||||
import {
|
import {
|
||||||
tokenServiceFactory,
|
tokenServiceFactory,
|
||||||
TokenServiceInitOptions,
|
TokenServiceInitOptions,
|
||||||
|
@ -34,6 +42,8 @@ import {
|
||||||
type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions;
|
type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions &
|
export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions &
|
||||||
|
AccountServiceInitOptions &
|
||||||
|
PinServiceInitOptions &
|
||||||
UserDecryptionOptionsServiceInitOptions &
|
UserDecryptionOptionsServiceInitOptions &
|
||||||
CryptoServiceInitOptions &
|
CryptoServiceInitOptions &
|
||||||
TokenServiceInitOptions &
|
TokenServiceInitOptions &
|
||||||
|
@ -51,6 +61,8 @@ export function vaultTimeoutSettingsServiceFactory(
|
||||||
opts,
|
opts,
|
||||||
async () =>
|
async () =>
|
||||||
new VaultTimeoutSettingsService(
|
new VaultTimeoutSettingsService(
|
||||||
|
await accountServiceFactory(cache, opts),
|
||||||
|
await pinServiceFactory(cache, opts),
|
||||||
await userDecryptionOptionsServiceFactory(cache, opts),
|
await userDecryptionOptionsServiceFactory(cache, opts),
|
||||||
await cryptoServiceFactory(cache, opts),
|
await cryptoServiceFactory(cache, opts),
|
||||||
await tokenServiceFactory(cache, opts),
|
await tokenServiceFactory(cache, opts),
|
||||||
|
|
|
@ -12,6 +12,10 @@ import {
|
||||||
internalMasterPasswordServiceFactory,
|
internalMasterPasswordServiceFactory,
|
||||||
MasterPasswordServiceInitOptions,
|
MasterPasswordServiceInitOptions,
|
||||||
} from "../../../auth/background/service-factories/master-password-service.factory";
|
} from "../../../auth/background/service-factories/master-password-service.factory";
|
||||||
|
import {
|
||||||
|
PinServiceInitOptions,
|
||||||
|
pinServiceFactory,
|
||||||
|
} from "../../../auth/background/service-factories/pin-service.factory";
|
||||||
import {
|
import {
|
||||||
StateServiceInitOptions,
|
StateServiceInitOptions,
|
||||||
stateServiceFactory,
|
stateServiceFactory,
|
||||||
|
@ -45,6 +49,7 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider
|
||||||
type CryptoServiceFactoryOptions = FactoryOptions;
|
type CryptoServiceFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
export type CryptoServiceInitOptions = CryptoServiceFactoryOptions &
|
export type CryptoServiceInitOptions = CryptoServiceFactoryOptions &
|
||||||
|
PinServiceInitOptions &
|
||||||
MasterPasswordServiceInitOptions &
|
MasterPasswordServiceInitOptions &
|
||||||
KeyGenerationServiceInitOptions &
|
KeyGenerationServiceInitOptions &
|
||||||
CryptoFunctionServiceInitOptions &
|
CryptoFunctionServiceInitOptions &
|
||||||
|
@ -67,6 +72,7 @@ export function cryptoServiceFactory(
|
||||||
opts,
|
opts,
|
||||||
async () =>
|
async () =>
|
||||||
new BrowserCryptoService(
|
new BrowserCryptoService(
|
||||||
|
await pinServiceFactory(cache, opts),
|
||||||
await internalMasterPasswordServiceFactory(cache, opts),
|
await internalMasterPasswordServiceFactory(cache, opts),
|
||||||
await keyGenerationServiceFactory(cache, opts),
|
await keyGenerationServiceFactory(cache, opts),
|
||||||
await cryptoFunctionServiceFactory(cache, opts),
|
await cryptoFunctionServiceFactory(cache, opts),
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
|
@ -19,6 +20,7 @@ import { UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
export class BrowserCryptoService extends CryptoService {
|
export class BrowserCryptoService extends CryptoService {
|
||||||
constructor(
|
constructor(
|
||||||
|
pinService: PinServiceAbstraction,
|
||||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
keyGenerationService: KeyGenerationService,
|
keyGenerationService: KeyGenerationService,
|
||||||
cryptoFunctionService: CryptoFunctionService,
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
|
@ -32,6 +34,7 @@ export class BrowserCryptoService extends CryptoService {
|
||||||
kdfConfigService: KdfConfigService,
|
kdfConfigService: KdfConfigService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
|
pinService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
keyGenerationService,
|
keyGenerationService,
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
|
|
|
@ -163,6 +163,10 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
||||||
* the view is open.
|
* the view is open.
|
||||||
*/
|
*/
|
||||||
async isViewOpen(): Promise<boolean> {
|
async isViewOpen(): Promise<boolean> {
|
||||||
|
if (this.isSafari()) {
|
||||||
|
// Query views on safari since chrome.runtime.sendMessage does not timeout and will hang.
|
||||||
|
return BrowserApi.isPopupOpen();
|
||||||
|
}
|
||||||
return Boolean(await BrowserApi.sendMessageWithResponse("checkVaultPopupHeartbeat"));
|
return Boolean(await BrowserApi.sendMessageWithResponse("checkVaultPopupHeartbeat"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -196,12 +196,13 @@ export const routerTransition = trigger("routerTransition", [
|
||||||
transition("vault-settings => sync", inSlideLeft),
|
transition("vault-settings => sync", inSlideLeft),
|
||||||
transition("sync => vault-settings", outSlideRight),
|
transition("sync => vault-settings", outSlideRight),
|
||||||
|
|
||||||
transition("tabs => excluded-domains", inSlideLeft),
|
|
||||||
transition("excluded-domains => tabs", outSlideRight),
|
|
||||||
|
|
||||||
transition("tabs => options", inSlideLeft),
|
transition("tabs => options", inSlideLeft),
|
||||||
transition("options => tabs", outSlideRight),
|
transition("options => tabs", outSlideRight),
|
||||||
|
|
||||||
|
// Appearance settings
|
||||||
|
transition("tabs => appearance", inSlideLeft),
|
||||||
|
transition("appearance => tabs", outSlideRight),
|
||||||
|
|
||||||
transition("tabs => premium", inSlideLeft),
|
transition("tabs => premium", inSlideLeft),
|
||||||
transition("premium => tabs", outSlideRight),
|
transition("premium => tabs", outSlideRight),
|
||||||
|
|
||||||
|
@ -219,6 +220,13 @@ export const routerTransition = trigger("routerTransition", [
|
||||||
transition("tabs => edit-send, send-type => edit-send", inSlideUp),
|
transition("tabs => edit-send, send-type => edit-send", inSlideUp),
|
||||||
transition("edit-send => tabs, edit-send => send-type", outSlideDown),
|
transition("edit-send => tabs, edit-send => send-type", outSlideDown),
|
||||||
|
|
||||||
|
// Notification settings
|
||||||
|
transition("tabs => notifications", inSlideLeft),
|
||||||
|
transition("notifications => tabs", outSlideRight),
|
||||||
|
|
||||||
|
transition("notifications => excluded-domains", inSlideLeft),
|
||||||
|
transition("excluded-domains => notifications", outSlideRight),
|
||||||
|
|
||||||
transition("tabs => autofill", inSlideLeft),
|
transition("tabs => autofill", inSlideLeft),
|
||||||
transition("autofill => tabs", outSlideRight),
|
transition("autofill => tabs", outSlideRight),
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,8 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp
|
||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
|
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
||||||
|
import { NotifcationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||||
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
||||||
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
||||||
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
import { GeneratorComponent } from "../tools/popup/generator/generator.component";
|
||||||
|
@ -46,7 +48,9 @@ import { PasswordHistoryComponent } from "../vault/popup/components/vault/passwo
|
||||||
import { ShareComponent } from "../vault/popup/components/vault/share.component";
|
import { ShareComponent } from "../vault/popup/components/vault/share.component";
|
||||||
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
||||||
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
||||||
|
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
|
||||||
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
||||||
|
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||||
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
||||||
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
||||||
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
||||||
|
@ -54,7 +58,6 @@ import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.c
|
||||||
|
|
||||||
import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils";
|
import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils";
|
||||||
import { debounceNavigationGuard } from "./services/debounce-navigation.service";
|
import { debounceNavigationGuard } from "./services/debounce-navigation.service";
|
||||||
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
|
||||||
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
||||||
import { OptionsComponent } from "./settings/options.component";
|
import { OptionsComponent } from "./settings/options.component";
|
||||||
import { TabsV2Component } from "./tabs-v2.component";
|
import { TabsV2Component } from "./tabs-v2.component";
|
||||||
|
@ -254,6 +257,12 @@ const routes: Routes = [
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "account-security" },
|
data: { state: "account-security" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "notifications",
|
||||||
|
component: NotifcationsSettingsComponent,
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
data: { state: "notifications" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "vault-settings",
|
path: "vault-settings",
|
||||||
component: VaultSettingsComponent,
|
component: VaultSettingsComponent,
|
||||||
|
@ -302,6 +311,12 @@ const routes: Routes = [
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "options" },
|
data: { state: "options" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "appearance",
|
||||||
|
component: AppearanceComponent,
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
data: { state: "appearance" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "clone-cipher",
|
path: "clone-cipher",
|
||||||
component: AddEditComponent,
|
component: AddEditComponent,
|
||||||
|
@ -355,12 +370,11 @@ const routes: Routes = [
|
||||||
data: { state: "tabs_current" },
|
data: { state: "tabs_current" },
|
||||||
runGuardsAndResolvers: "always",
|
runGuardsAndResolvers: "always",
|
||||||
},
|
},
|
||||||
{
|
...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, {
|
||||||
path: "vault",
|
path: "vault",
|
||||||
component: VaultFilterComponent,
|
|
||||||
canActivate: [AuthGuard],
|
canActivate: [AuthGuard],
|
||||||
data: { state: "tabs_vault" },
|
data: { state: "tabs_vault" },
|
||||||
},
|
}),
|
||||||
{
|
{
|
||||||
path: "generator",
|
path: "generator",
|
||||||
component: GeneratorComponent,
|
component: GeneratorComponent,
|
||||||
|
|
|
@ -37,6 +37,8 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp
|
||||||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
|
||||||
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
|
||||||
|
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
|
||||||
|
import { NotifcationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
|
||||||
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
import { PremiumComponent } from "../billing/popup/settings/premium.component";
|
||||||
import { HeaderComponent } from "../platform/popup/header.component";
|
import { HeaderComponent } from "../platform/popup/header.component";
|
||||||
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
|
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
|
||||||
|
@ -67,8 +69,10 @@ import { ShareComponent } from "../vault/popup/components/vault/share.component"
|
||||||
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
|
||||||
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
|
||||||
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
|
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
|
||||||
|
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
|
||||||
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
|
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
|
||||||
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
import { ViewComponent } from "../vault/popup/components/vault/view.component";
|
||||||
|
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||||
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
||||||
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
import { FoldersComponent } from "../vault/popup/settings/folders.component";
|
||||||
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
import { SyncComponent } from "../vault/popup/settings/sync.component";
|
||||||
|
@ -79,7 +83,6 @@ import { AppComponent } from "./app.component";
|
||||||
import { PopOutComponent } from "./components/pop-out.component";
|
import { PopOutComponent } from "./components/pop-out.component";
|
||||||
import { UserVerificationComponent } from "./components/user-verification.component";
|
import { UserVerificationComponent } from "./components/user-verification.component";
|
||||||
import { ServicesModule } from "./services/services.module";
|
import { ServicesModule } from "./services/services.module";
|
||||||
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
|
||||||
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
||||||
import { OptionsComponent } from "./settings/options.component";
|
import { OptionsComponent } from "./settings/options.component";
|
||||||
import { TabsV2Component } from "./tabs-v2.component";
|
import { TabsV2Component } from "./tabs-v2.component";
|
||||||
|
@ -147,6 +150,8 @@ import "../platform/popup/locales";
|
||||||
LoginViaAuthRequestComponent,
|
LoginViaAuthRequestComponent,
|
||||||
LoginDecryptionOptionsComponent,
|
LoginDecryptionOptionsComponent,
|
||||||
OptionsComponent,
|
OptionsComponent,
|
||||||
|
NotifcationsSettingsComponent,
|
||||||
|
AppearanceComponent,
|
||||||
GeneratorComponent,
|
GeneratorComponent,
|
||||||
PasswordGeneratorHistoryComponent,
|
PasswordGeneratorHistoryComponent,
|
||||||
PasswordHistoryComponent,
|
PasswordHistoryComponent,
|
||||||
|
@ -181,6 +186,7 @@ import "../platform/popup/locales";
|
||||||
EnvironmentSelectorComponent,
|
EnvironmentSelectorComponent,
|
||||||
CurrentAccountComponent,
|
CurrentAccountComponent,
|
||||||
AccountSwitcherComponent,
|
AccountSwitcherComponent,
|
||||||
|
VaultV2Component,
|
||||||
],
|
],
|
||||||
providers: [CurrencyPipe, DatePipe],
|
providers: [CurrencyPipe, DatePipe],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
|
|
|
@ -424,6 +424,10 @@ img,
|
||||||
.modal-title,
|
.modal-title,
|
||||||
.overlay-container {
|
.overlay-container {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
|
&.user-select {
|
||||||
|
user-select: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app-about .modal-body > *,
|
app-about .modal-body > *,
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
CLIENT_TYPE,
|
CLIENT_TYPE,
|
||||||
} from "@bitwarden/angular/services/injection-tokens";
|
} from "@bitwarden/angular/services/injection-tokens";
|
||||||
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
|
||||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
import { AuthRequestServiceAbstraction, PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
|
@ -209,6 +209,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: CryptoService,
|
provide: CryptoService,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
|
pinService: PinServiceAbstraction,
|
||||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
keyGenerationService: KeyGenerationService,
|
keyGenerationService: KeyGenerationService,
|
||||||
cryptoFunctionService: CryptoFunctionService,
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
|
@ -222,6 +223,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
kdfConfigService: KdfConfigService,
|
kdfConfigService: KdfConfigService,
|
||||||
) => {
|
) => {
|
||||||
const cryptoService = new BrowserCryptoService(
|
const cryptoService = new BrowserCryptoService(
|
||||||
|
pinService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
keyGenerationService,
|
keyGenerationService,
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
|
@ -238,6 +240,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
return cryptoService;
|
return cryptoService;
|
||||||
},
|
},
|
||||||
deps: [
|
deps: [
|
||||||
|
PinServiceAbstraction,
|
||||||
InternalMasterPasswordServiceAbstraction,
|
InternalMasterPasswordServiceAbstraction,
|
||||||
KeyGenerationService,
|
KeyGenerationService,
|
||||||
CryptoFunctionService,
|
CryptoFunctionService,
|
||||||
|
|
|
@ -60,67 +60,6 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="totpHelp" class="box-footer">{{ "disableAutoTotpCopyDesc" | i18n }}</div>
|
<div id="totpHelp" class="box-footer">{{ "disableAutoTotpCopyDesc" | i18n }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="addlogin-notification-bar">{{ "enableAddLoginNotification" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="addlogin-notification-bar"
|
|
||||||
type="checkbox"
|
|
||||||
aria-describedby="addlogin-notification-barHelp"
|
|
||||||
(change)="updateAddLoginNotification()"
|
|
||||||
[(ngModel)]="enableAddLoginNotification"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="addlogin-notification-barHelp" class="box-footer">
|
|
||||||
{{
|
|
||||||
accountSwitcherEnabled
|
|
||||||
? ("addLoginNotificationDescAlt" | i18n)
|
|
||||||
: ("addLoginNotificationDesc" | i18n)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="changedpass-notification-bar">{{
|
|
||||||
"enableChangedPasswordNotification" | i18n
|
|
||||||
}}</label>
|
|
||||||
<input
|
|
||||||
id="changedpass-notification-bar"
|
|
||||||
type="checkbox"
|
|
||||||
aria-describedby="changedpass-notification-barHelp"
|
|
||||||
(change)="updateChangedPasswordNotification()"
|
|
||||||
[(ngModel)]="enableChangedPasswordNotification"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="changedpass-notification-barHelp" class="box-footer">
|
|
||||||
{{
|
|
||||||
accountSwitcherEnabled
|
|
||||||
? ("changedPasswordNotificationDescAlt" | i18n)
|
|
||||||
: ("changedPasswordNotificationDesc" | i18n)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="use-passkeys">{{ "enableUsePasskeys" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="use-passkeys"
|
|
||||||
type="checkbox"
|
|
||||||
aria-describedby="use-passkeysHelp"
|
|
||||||
(change)="updateEnablePasskeys()"
|
|
||||||
[(ngModel)]="enablePasskeys"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="use-passkeysHelp" class="box-footer">
|
|
||||||
{{ "usePasskeysDesc" | i18n }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<div class="box-content">
|
<div class="box-content">
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
@ -192,56 +131,5 @@
|
||||||
{{ "showIdentitiesCurrentTabDesc" | i18n }}
|
{{ "showIdentitiesCurrentTabDesc" | i18n }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="favicon">{{ "enableFavicon" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="favicon"
|
|
||||||
type="checkbox"
|
|
||||||
aria-describedby="faviconHelp"
|
|
||||||
(change)="updateFavicon()"
|
|
||||||
[(ngModel)]="enableFavicon"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="faviconHelp" class="box-footer">
|
|
||||||
{{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
|
||||||
<label for="badge">{{ "enableBadgeCounter" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="badge"
|
|
||||||
type="checkbox"
|
|
||||||
aria-describedby="badgeHelp"
|
|
||||||
(change)="updateBadgeCounter()"
|
|
||||||
[(ngModel)]="enableBadgeCounter"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="badgeHelp" class="box-footer">{{ "badgeCounterDesc" | i18n }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="box">
|
|
||||||
<div class="box-content">
|
|
||||||
<div class="box-content-row" appBoxRow>
|
|
||||||
<label for="theme">{{ "theme" | i18n }}</label>
|
|
||||||
<select
|
|
||||||
id="theme"
|
|
||||||
name="Theme"
|
|
||||||
aria-describedby="themeHelp"
|
|
||||||
[(ngModel)]="theme"
|
|
||||||
(change)="saveTheme()"
|
|
||||||
>
|
|
||||||
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="themeHelp" class="box-footer">
|
|
||||||
{{ accountSwitcherEnabled ? ("themeDescAlt" | i18n) : ("themeDesc" | i18n) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -2,9 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
|
||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
|
||||||
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
|
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
|
||||||
import {
|
import {
|
||||||
UriMatchStrategy,
|
UriMatchStrategy,
|
||||||
|
@ -12,8 +10,6 @@ import {
|
||||||
} from "@bitwarden/common/models/domain/domain-service";
|
} from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
|
||||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
|
|
||||||
import { enableAccountSwitching } from "../../platform/flags";
|
import { enableAccountSwitching } from "../../platform/flags";
|
||||||
|
@ -23,47 +19,29 @@ import { enableAccountSwitching } from "../../platform/flags";
|
||||||
templateUrl: "options.component.html",
|
templateUrl: "options.component.html",
|
||||||
})
|
})
|
||||||
export class OptionsComponent implements OnInit {
|
export class OptionsComponent implements OnInit {
|
||||||
enableFavicon = false;
|
|
||||||
enableBadgeCounter = true;
|
|
||||||
enableAutoFillOnPageLoad = false;
|
enableAutoFillOnPageLoad = false;
|
||||||
autoFillOnPageLoadDefault = false;
|
autoFillOnPageLoadDefault = false;
|
||||||
autoFillOnPageLoadOptions: any[];
|
autoFillOnPageLoadOptions: any[];
|
||||||
enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true?
|
enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true?
|
||||||
enableContextMenuItem = false;
|
enableContextMenuItem = false;
|
||||||
enableAddLoginNotification = false;
|
|
||||||
enableChangedPasswordNotification = false;
|
|
||||||
enablePasskeys = true;
|
|
||||||
showCardsCurrentTab = false;
|
showCardsCurrentTab = false;
|
||||||
showIdentitiesCurrentTab = false;
|
showIdentitiesCurrentTab = false;
|
||||||
showClearClipboard = true;
|
showClearClipboard = true;
|
||||||
theme: ThemeType;
|
|
||||||
themeOptions: any[];
|
|
||||||
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
|
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
|
||||||
uriMatchOptions: any[];
|
uriMatchOptions: any[];
|
||||||
clearClipboard: ClearClipboardDelaySetting;
|
clearClipboard: ClearClipboardDelaySetting;
|
||||||
clearClipboardOptions: any[];
|
clearClipboardOptions: any[];
|
||||||
showGeneral = true;
|
showGeneral = true;
|
||||||
showAutofill = true;
|
|
||||||
showDisplay = true;
|
showDisplay = true;
|
||||||
accountSwitcherEnabled = false;
|
accountSwitcherEnabled = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
private domainSettingsService: DomainSettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
private themeStateService: ThemeStateService,
|
|
||||||
private vaultSettingsService: VaultSettingsService,
|
private vaultSettingsService: VaultSettingsService,
|
||||||
) {
|
) {
|
||||||
this.themeOptions = [
|
|
||||||
{ name: i18nService.t("default"), value: ThemeType.System },
|
|
||||||
{ name: i18nService.t("light"), value: ThemeType.Light },
|
|
||||||
{ name: i18nService.t("dark"), value: ThemeType.Dark },
|
|
||||||
{ name: "Nord", value: ThemeType.Nord },
|
|
||||||
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
|
|
||||||
];
|
|
||||||
this.uriMatchOptions = [
|
this.uriMatchOptions = [
|
||||||
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
||||||
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
||||||
|
@ -98,14 +76,6 @@ export class OptionsComponent implements OnInit {
|
||||||
this.autofillSettingsService.autofillOnPageLoadDefault$,
|
this.autofillSettingsService.autofillOnPageLoadDefault$,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.enableAddLoginNotification = await firstValueFrom(
|
|
||||||
this.userNotificationSettingsService.enableAddedLoginPrompt$,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.enableChangedPasswordNotification = await firstValueFrom(
|
|
||||||
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.enableContextMenuItem = await firstValueFrom(
|
this.enableContextMenuItem = await firstValueFrom(
|
||||||
this.autofillSettingsService.enableContextMenu$,
|
this.autofillSettingsService.enableContextMenu$,
|
||||||
);
|
);
|
||||||
|
@ -117,14 +87,6 @@ export class OptionsComponent implements OnInit {
|
||||||
|
|
||||||
this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$);
|
this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$);
|
||||||
|
|
||||||
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
|
||||||
|
|
||||||
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
|
|
||||||
|
|
||||||
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
|
|
||||||
|
|
||||||
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
|
|
||||||
|
|
||||||
const defaultUriMatch = await firstValueFrom(
|
const defaultUriMatch = await firstValueFrom(
|
||||||
this.domainSettingsService.defaultUriMatchStrategy$,
|
this.domainSettingsService.defaultUriMatchStrategy$,
|
||||||
);
|
);
|
||||||
|
@ -133,22 +95,6 @@ export class OptionsComponent implements OnInit {
|
||||||
this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$);
|
this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAddLoginNotification() {
|
|
||||||
await this.userNotificationSettingsService.setEnableAddedLoginPrompt(
|
|
||||||
this.enableAddLoginNotification,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateChangedPasswordNotification() {
|
|
||||||
await this.userNotificationSettingsService.setEnableChangedPasswordPrompt(
|
|
||||||
this.enableChangedPasswordNotification,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateEnablePasskeys() {
|
|
||||||
await this.vaultSettingsService.setEnablePasskeys(this.enablePasskeys);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateContextMenuItem() {
|
async updateContextMenuItem() {
|
||||||
await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem);
|
await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem);
|
||||||
this.messagingService.send("bgUpdateContextMenu");
|
this.messagingService.send("bgUpdateContextMenu");
|
||||||
|
@ -166,15 +112,6 @@ export class OptionsComponent implements OnInit {
|
||||||
await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault);
|
await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateFavicon() {
|
|
||||||
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateBadgeCounter() {
|
|
||||||
await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter);
|
|
||||||
this.messagingService.send("bgUpdateContextMenu");
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateShowCardsCurrentTab() {
|
async updateShowCardsCurrentTab() {
|
||||||
await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab);
|
await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab);
|
||||||
}
|
}
|
||||||
|
@ -183,10 +120,6 @@ export class OptionsComponent implements OnInit {
|
||||||
await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab);
|
await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveTheme() {
|
|
||||||
await this.themeStateService.setSelectedTheme(this.theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveClearClipboard() {
|
async saveClearClipboard() {
|
||||||
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
|
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { ImportService, ImportServiceAbstraction } from "@bitwarden/importer/core";
|
import { ImportService, ImportServiceAbstraction } from "@bitwarden/importer/core";
|
||||||
|
|
||||||
|
import {
|
||||||
|
pinServiceFactory,
|
||||||
|
PinServiceInitOptions,
|
||||||
|
} from "../../../auth/background/service-factories/pin-service.factory";
|
||||||
import {
|
import {
|
||||||
cryptoServiceFactory,
|
cryptoServiceFactory,
|
||||||
CryptoServiceInitOptions,
|
CryptoServiceInitOptions,
|
||||||
|
@ -36,7 +40,8 @@ export type ImportServiceInitOptions = ImportServiceFactoryOptions &
|
||||||
ImportApiServiceInitOptions &
|
ImportApiServiceInitOptions &
|
||||||
I18nServiceInitOptions &
|
I18nServiceInitOptions &
|
||||||
CollectionServiceInitOptions &
|
CollectionServiceInitOptions &
|
||||||
CryptoServiceInitOptions;
|
CryptoServiceInitOptions &
|
||||||
|
PinServiceInitOptions;
|
||||||
|
|
||||||
export function importServiceFactory(
|
export function importServiceFactory(
|
||||||
cache: {
|
cache: {
|
||||||
|
@ -56,6 +61,7 @@ export function importServiceFactory(
|
||||||
await i18nServiceFactory(cache, opts),
|
await i18nServiceFactory(cache, opts),
|
||||||
await collectionServiceFactory(cache, opts),
|
await collectionServiceFactory(cache, opts),
|
||||||
await cryptoServiceFactory(cache, opts),
|
await cryptoServiceFactory(cache, opts),
|
||||||
|
await pinServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<div bitDialogTitle>Bitwarden</div>
|
<div bitDialogTitle>Bitwarden</div>
|
||||||
<div bitDialogContent>
|
<div bitDialogContent>
|
||||||
<p>© Bitwarden Inc. 2015-{{ year }}</p>
|
<p>© Bitwarden Inc. 2015-{{ year }}</p>
|
||||||
<p>{{ "version" | i18n }}: {{ version$ | async }}</p>
|
<p class="user-select">{{ "version" | i18n }}: {{ version$ | async }}</p>
|
||||||
<ng-container *ngIf="data$ | async as data">
|
<ng-container *ngIf="data$ | async as data">
|
||||||
<p *ngIf="data.isCloud">
|
<p *ngIf="data.isCloud">
|
||||||
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
|
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
<ng-container *ngIf="!data.isCloud">
|
<ng-container *ngIf="!data.isCloud">
|
||||||
<ng-container *ngIf="data.serverConfig.server">
|
<ng-container *ngIf="data.serverConfig.server">
|
||||||
<p>
|
<p class="user-select">
|
||||||
{{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>:
|
{{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>:
|
||||||
{{ data.serverConfig?.version }}
|
{{ data.serverConfig?.version }}
|
||||||
<span *ngIf="!data.serverConfig.isValid()">
|
<span *ngIf="!data.serverConfig.isValid()">
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<p *ngIf="!data.serverConfig.server">
|
<p class="user-select" *ngIf="!data.serverConfig.server">
|
||||||
{{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>:
|
{{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>:
|
||||||
{{ data.serverConfig?.version }}
|
{{ data.serverConfig?.version }}
|
||||||
<span *ngIf="!data.serverConfig.isValid()">
|
<span *ngIf="!data.serverConfig.isValid()">
|
|
@ -9,11 +9,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||||
import { ButtonModule, DialogModule } from "@bitwarden/components";
|
import { ButtonModule, DialogModule } from "@bitwarden/components";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
templateUrl: "about.component.html",
|
templateUrl: "about-dialog.component.html",
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, JslibModule, DialogModule, ButtonModule],
|
imports: [CommonModule, JslibModule, DialogModule, ButtonModule],
|
||||||
})
|
})
|
||||||
export class AboutComponent {
|
export class AboutDialogComponent {
|
||||||
protected year = new Date().getFullYear();
|
protected year = new Date().getFullYear();
|
||||||
protected version$: Observable<string>;
|
protected version$: Observable<string>;
|
||||||
|
|
|
@ -30,17 +30,17 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="box-content-row box-content-row-flex text-default"
|
class="box-content-row box-content-row-flex text-default"
|
||||||
routerLink="/vault-settings"
|
routerLink="/notifications"
|
||||||
>
|
>
|
||||||
<div class="row-main">{{ "vault" | i18n }}</div>
|
<div class="row-main">{{ "notifications" | i18n }}</div>
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="box-content-row box-content-row-flex text-default"
|
class="box-content-row box-content-row-flex text-default"
|
||||||
routerLink="/excluded-domains"
|
routerLink="/vault-settings"
|
||||||
>
|
>
|
||||||
<div class="row-main">{{ "excludedDomains" | i18n }}</div>
|
<div class="row-main">{{ "vault" | i18n }}</div>
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -86,6 +86,14 @@
|
||||||
<div class="row-main">{{ "options" | i18n }}</div>
|
<div class="row-main">{{ "options" | i18n }}</div>
|
||||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
routerLink="/appearance"
|
||||||
|
>
|
||||||
|
<div class="row-main">{{ "appearance" | i18n }}</div>
|
||||||
|
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="box-content-row box-content-row-flex text-default"
|
class="box-content-row box-content-row-flex text-default"
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { DialogService } from "@bitwarden/components";
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||||
|
|
||||||
import { AboutComponent } from "./about/about.component";
|
import { AboutDialogComponent } from "./about-dialog/about-dialog.component";
|
||||||
|
|
||||||
const RateUrls = {
|
const RateUrls = {
|
||||||
[DeviceType.ChromeExtension]:
|
[DeviceType.ChromeExtension]:
|
||||||
|
@ -84,7 +84,7 @@ export class SettingsComponent implements OnInit {
|
||||||
}
|
}
|
||||||
|
|
||||||
about() {
|
about() {
|
||||||
this.dialogService.open(AboutComponent);
|
this.dialogService.open(AboutDialogComponent);
|
||||||
}
|
}
|
||||||
|
|
||||||
rate() {
|
rate() {
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
<h1>Vault V2 Extension Refresh</h1>
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-vault",
|
||||||
|
templateUrl: "vault-v2.component.html",
|
||||||
|
})
|
||||||
|
export class VaultV2Component implements OnInit, OnDestroy {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
ngOnInit(): void {}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {}
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
<header>
|
||||||
|
<div class="left">
|
||||||
|
<button type="button" routerLink="/tabs/settings">
|
||||||
|
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
|
||||||
|
<span>{{ "back" | i18n }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<h1 class="center">
|
||||||
|
<span class="title">{{ "appearance" | i18n }}</span>
|
||||||
|
</h1>
|
||||||
|
<div class="right">
|
||||||
|
<app-pop-out></app-pop-out>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main tabindex="-1">
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row" appBoxRow>
|
||||||
|
<label for="theme">{{ "theme" | i18n }}</label>
|
||||||
|
<select
|
||||||
|
id="theme"
|
||||||
|
name="Theme"
|
||||||
|
aria-describedby="themeHelp"
|
||||||
|
[(ngModel)]="theme"
|
||||||
|
(change)="saveTheme()"
|
||||||
|
>
|
||||||
|
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="themeHelp" class="box-footer">
|
||||||
|
{{ accountSwitcherEnabled ? ("themeDescAlt" | i18n) : ("themeDesc" | i18n) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="badge">{{ "enableBadgeCounter" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="badge"
|
||||||
|
type="checkbox"
|
||||||
|
aria-describedby="badgeHelp"
|
||||||
|
(change)="updateBadgeCounter()"
|
||||||
|
[(ngModel)]="enableBadgeCounter"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="badgeHelp" class="box-footer">{{ "badgeCounterDesc" | i18n }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||||
|
<label for="favicon">{{ "enableFavicon" | i18n }}</label>
|
||||||
|
<input
|
||||||
|
id="favicon"
|
||||||
|
type="checkbox"
|
||||||
|
aria-describedby="faviconHelp"
|
||||||
|
(change)="updateFavicon()"
|
||||||
|
[(ngModel)]="enableFavicon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="faviconHelp" class="box-footer">
|
||||||
|
{{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { Component, OnInit } from "@angular/core";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
|
|
||||||
|
import { enableAccountSwitching } from "../../../platform/flags";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "vault-appearance",
|
||||||
|
templateUrl: "appearance.component.html",
|
||||||
|
})
|
||||||
|
export class AppearanceComponent implements OnInit {
|
||||||
|
enableFavicon = false;
|
||||||
|
enableBadgeCounter = true;
|
||||||
|
theme: ThemeType;
|
||||||
|
themeOptions: any[];
|
||||||
|
accountSwitcherEnabled = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
|
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
||||||
|
i18nService: I18nService,
|
||||||
|
private themeStateService: ThemeStateService,
|
||||||
|
) {
|
||||||
|
this.themeOptions = [
|
||||||
|
{ name: i18nService.t("default"), value: ThemeType.System },
|
||||||
|
{ name: i18nService.t("light"), value: ThemeType.Light },
|
||||||
|
{ name: i18nService.t("dark"), value: ThemeType.Dark },
|
||||||
|
{ name: "Nord", value: ThemeType.Nord },
|
||||||
|
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
|
||||||
|
];
|
||||||
|
|
||||||
|
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
|
||||||
|
|
||||||
|
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
|
||||||
|
|
||||||
|
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateFavicon() {
|
||||||
|
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateBadgeCounter() {
|
||||||
|
await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter);
|
||||||
|
this.messagingService.send("bgUpdateContextMenu");
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveTheme() {
|
||||||
|
await this.themeStateService.setSelectedTheme(this.theme);
|
||||||
|
}
|
||||||
|
}
|
|
@ -86,7 +86,7 @@ export class UnlockCommand {
|
||||||
|
|
||||||
if (passwordValid) {
|
if (passwordValid) {
|
||||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
await this.cryptoService.setUserKey(userKey);
|
await this.cryptoService.setUserKey(userKey);
|
||||||
|
|
||||||
if (await this.keyConnectorService.getConvertAccountRequired()) {
|
if (await this.keyConnectorService.getConvertAccountRequired()) {
|
||||||
|
|
|
@ -10,8 +10,8 @@ import {
|
||||||
AuthRequestService,
|
AuthRequestService,
|
||||||
LoginStrategyService,
|
LoginStrategyService,
|
||||||
LoginStrategyServiceAbstraction,
|
LoginStrategyServiceAbstraction,
|
||||||
PinCryptoService,
|
PinService,
|
||||||
PinCryptoServiceAbstraction,
|
PinServiceAbstraction,
|
||||||
UserDecryptionOptionsService,
|
UserDecryptionOptionsService,
|
||||||
} from "@bitwarden/auth/common";
|
} from "@bitwarden/auth/common";
|
||||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
|
@ -208,7 +208,7 @@ export class Main {
|
||||||
cipherFileUploadService: CipherFileUploadService;
|
cipherFileUploadService: CipherFileUploadService;
|
||||||
keyConnectorService: KeyConnectorService;
|
keyConnectorService: KeyConnectorService;
|
||||||
userVerificationService: UserVerificationService;
|
userVerificationService: UserVerificationService;
|
||||||
pinCryptoService: PinCryptoServiceAbstraction;
|
pinService: PinServiceAbstraction;
|
||||||
stateService: StateService;
|
stateService: StateService;
|
||||||
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
||||||
domainSettingsService: DomainSettingsService;
|
domainSettingsService: DomainSettingsService;
|
||||||
|
@ -363,11 +363,29 @@ export class Main {
|
||||||
migrationRunner,
|
migrationRunner,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.masterPasswordService = new MasterPasswordService(this.stateProvider);
|
this.masterPasswordService = new MasterPasswordService(
|
||||||
|
this.stateProvider,
|
||||||
|
this.stateService,
|
||||||
|
this.keyGenerationService,
|
||||||
|
this.encryptService,
|
||||||
|
);
|
||||||
|
|
||||||
this.kdfConfigService = new KdfConfigService(this.stateProvider);
|
this.kdfConfigService = new KdfConfigService(this.stateProvider);
|
||||||
|
|
||||||
|
this.pinService = new PinService(
|
||||||
|
this.accountService,
|
||||||
|
this.cryptoFunctionService,
|
||||||
|
this.encryptService,
|
||||||
|
this.kdfConfigService,
|
||||||
|
this.keyGenerationService,
|
||||||
|
this.logService,
|
||||||
|
this.masterPasswordService,
|
||||||
|
this.stateProvider,
|
||||||
|
this.stateService,
|
||||||
|
);
|
||||||
|
|
||||||
this.cryptoService = new CryptoService(
|
this.cryptoService = new CryptoService(
|
||||||
|
this.pinService,
|
||||||
this.masterPasswordService,
|
this.masterPasswordService,
|
||||||
this.keyGenerationService,
|
this.keyGenerationService,
|
||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
|
@ -582,6 +600,8 @@ export class Main {
|
||||||
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
||||||
|
|
||||||
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
||||||
|
this.accountService,
|
||||||
|
this.pinService,
|
||||||
this.userDecryptionOptionsService,
|
this.userDecryptionOptionsService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.tokenService,
|
this.tokenService,
|
||||||
|
@ -590,14 +610,6 @@ export class Main {
|
||||||
this.biometricStateService,
|
this.biometricStateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.pinCryptoService = new PinCryptoService(
|
|
||||||
this.stateService,
|
|
||||||
this.cryptoService,
|
|
||||||
this.vaultTimeoutSettingsService,
|
|
||||||
this.logService,
|
|
||||||
this.kdfConfigService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.userVerificationService = new UserVerificationService(
|
this.userVerificationService = new UserVerificationService(
|
||||||
this.stateService,
|
this.stateService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
|
@ -606,7 +618,7 @@ export class Main {
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.userVerificationApiService,
|
this.userVerificationApiService,
|
||||||
this.userDecryptionOptionsService,
|
this.userDecryptionOptionsService,
|
||||||
this.pinCryptoService,
|
this.pinService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.vaultTimeoutSettingsService,
|
this.vaultTimeoutSettingsService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
|
@ -669,11 +681,13 @@ export class Main {
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.collectionService,
|
this.collectionService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
|
this.pinService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.individualExportService = new IndividualVaultExportService(
|
this.individualExportService = new IndividualVaultExportService(
|
||||||
this.folderService,
|
this.folderService,
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
|
this.pinService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
this.kdfConfigService,
|
this.kdfConfigService,
|
||||||
|
@ -682,6 +696,7 @@ export class Main {
|
||||||
this.organizationExportService = new OrganizationVaultExportService(
|
this.organizationExportService = new OrganizationVaultExportService(
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
|
this.pinService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.cryptoFunctionService,
|
this.cryptoFunctionService,
|
||||||
this.collectionService,
|
this.collectionService,
|
||||||
|
@ -693,6 +708,8 @@ export class Main {
|
||||||
this.organizationExportService,
|
this.organizationExportService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
|
||||||
|
|
||||||
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
|
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
|
||||||
this.program = new Program(this);
|
this.program = new Program(this);
|
||||||
this.vaultProgram = new VaultProgram(this);
|
this.vaultProgram = new VaultProgram(this);
|
||||||
|
@ -716,8 +733,6 @@ export class Main {
|
||||||
);
|
);
|
||||||
|
|
||||||
this.providerApiService = new ProviderApiService(this.apiService);
|
this.providerApiService = new ProviderApiService(this.apiService);
|
||||||
|
|
||||||
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import * as chalk from "chalk";
|
import * as chalk from "chalk";
|
||||||
import { program, Command, OptionValues } from "commander";
|
import { program, Command, OptionValues } from "commander";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
|
||||||
|
@ -63,8 +64,16 @@ export class Program {
|
||||||
process.env.BW_NOINTERACTION = "true";
|
process.env.BW_NOINTERACTION = "true";
|
||||||
});
|
});
|
||||||
|
|
||||||
program.on("option:session", (key) => {
|
program.on("option:session", async (key) => {
|
||||||
process.env.BW_SESSION = key;
|
process.env.BW_SESSION = key;
|
||||||
|
|
||||||
|
// once we have the session key, we can set the user key in memory
|
||||||
|
const activeAccount = await firstValueFrom(this.main.accountService.activeAccount$);
|
||||||
|
if (activeAccount) {
|
||||||
|
await this.main.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(
|
||||||
|
activeAccount.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
program.on("command:*", () => {
|
program.on("command:*", () => {
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs";
|
import { BehaviorSubject, Observable, Subject, firstValueFrom } from "rxjs";
|
||||||
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
|
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
|
||||||
|
|
||||||
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common";
|
import { AuthRequestServiceAbstraction, PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
@ -19,7 +20,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
import { KeySuffixOptions, ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
@ -111,6 +112,7 @@ export class SettingsComponent implements OnInit {
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
private policyService: PolicyService,
|
private policyService: PolicyService,
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
|
@ -127,6 +129,7 @@ export class SettingsComponent implements OnInit {
|
||||||
private desktopSettingsService: DesktopSettingsService,
|
private desktopSettingsService: DesktopSettingsService,
|
||||||
private biometricStateService: BiometricStateService,
|
private biometricStateService: BiometricStateService,
|
||||||
private desktopAutofillSettingsService: DesktopAutofillSettingsService,
|
private desktopAutofillSettingsService: DesktopAutofillSettingsService,
|
||||||
|
private pinService: PinServiceAbstraction,
|
||||||
private authRequestService: AuthRequestServiceAbstraction,
|
private authRequestService: AuthRequestServiceAbstraction,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private nativeMessagingManifestService: NativeMessagingManifestService,
|
private nativeMessagingManifestService: NativeMessagingManifestService,
|
||||||
|
@ -243,9 +246,10 @@ export class SettingsComponent implements OnInit {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
|
|
||||||
// Load initial values
|
// Load initial values
|
||||||
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
|
this.userHasPinSet = await this.pinService.isPinSet(userId);
|
||||||
this.userHasPinSet = pinStatus !== "DISABLED";
|
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(),
|
vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(),
|
||||||
|
|
|
@ -59,6 +59,7 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/ge
|
||||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions";
|
||||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||||
|
@ -183,6 +184,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
provide: SystemServiceAbstraction,
|
provide: SystemServiceAbstraction,
|
||||||
useClass: SystemService,
|
useClass: SystemService,
|
||||||
deps: [
|
deps: [
|
||||||
|
PinServiceAbstraction,
|
||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
RELOAD_CALLBACK,
|
RELOAD_CALLBACK,
|
||||||
|
@ -250,6 +252,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
provide: CryptoServiceAbstraction,
|
provide: CryptoServiceAbstraction,
|
||||||
useClass: ElectronCryptoService,
|
useClass: ElectronCryptoService,
|
||||||
deps: [
|
deps: [
|
||||||
|
PinServiceAbstraction,
|
||||||
InternalMasterPasswordServiceAbstraction,
|
InternalMasterPasswordServiceAbstraction,
|
||||||
KeyGenerationServiceAbstraction,
|
KeyGenerationServiceAbstraction,
|
||||||
CryptoFunctionServiceAbstraction,
|
CryptoFunctionServiceAbstraction,
|
||||||
|
|
|
@ -12,8 +12,16 @@
|
||||||
<input class="tw-font-mono" bitInput type="password" formControlName="pin" />
|
<input class="tw-font-mono" bitInput type="password" formControlName="pin" />
|
||||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||||
</bit-form-field>
|
</bit-form-field>
|
||||||
<label class="tw-flex tw-items-start tw-gap-2" *ngIf="showMasterPassOnRestart">
|
<label
|
||||||
<input class="tw-mt-1" type="checkbox" bitCheckbox formControlName="masterPassOnRestart" />
|
class="tw-flex tw-items-start tw-gap-2"
|
||||||
|
*ngIf="showMasterPasswordOnClientRestartOption"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class="tw-mt-1"
|
||||||
|
type="checkbox"
|
||||||
|
bitCheckbox
|
||||||
|
formControlName="requireMasterPasswordOnClientRestart"
|
||||||
|
/>
|
||||||
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
|
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { of } from "rxjs";
|
||||||
|
|
||||||
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
||||||
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
|
||||||
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
|
@ -155,8 +155,8 @@ describe("LockComponent", () => {
|
||||||
useValue: mock<UserVerificationService>(),
|
useValue: mock<UserVerificationService>(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: PinCryptoServiceAbstraction,
|
provide: PinServiceAbstraction,
|
||||||
useValue: mock<PinCryptoServiceAbstraction>(),
|
useValue: mock<PinServiceAbstraction>(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: BiometricStateService,
|
provide: BiometricStateService,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { firstValueFrom, switchMap } from "rxjs";
|
import { firstValueFrom, switchMap } from "rxjs";
|
||||||
|
|
||||||
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
||||||
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
|
@ -62,7 +62,7 @@ export class LockComponent extends BaseLockComponent {
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
deviceTrustService: DeviceTrustServiceAbstraction,
|
deviceTrustService: DeviceTrustServiceAbstraction,
|
||||||
userVerificationService: UserVerificationService,
|
userVerificationService: UserVerificationService,
|
||||||
pinCryptoService: PinCryptoServiceAbstraction,
|
pinService: PinServiceAbstraction,
|
||||||
biometricStateService: BiometricStateService,
|
biometricStateService: BiometricStateService,
|
||||||
accountService: AccountService,
|
accountService: AccountService,
|
||||||
authService: AuthService,
|
authService: AuthService,
|
||||||
|
@ -88,7 +88,7 @@ export class LockComponent extends BaseLockComponent {
|
||||||
dialogService,
|
dialogService,
|
||||||
deviceTrustService,
|
deviceTrustService,
|
||||||
userVerificationService,
|
userVerificationService,
|
||||||
pinCryptoService,
|
pinService,
|
||||||
biometricStateService,
|
biometricStateService,
|
||||||
accountService,
|
accountService,
|
||||||
authService,
|
authService,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
|
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
|
@ -26,6 +27,7 @@ import { ElectronCryptoService } from "./electron-crypto.service";
|
||||||
describe("electronCryptoService", () => {
|
describe("electronCryptoService", () => {
|
||||||
let sut: ElectronCryptoService;
|
let sut: ElectronCryptoService;
|
||||||
|
|
||||||
|
const pinService = mock<PinServiceAbstraction>();
|
||||||
const keyGenerationService = mock<KeyGenerationService>();
|
const keyGenerationService = mock<KeyGenerationService>();
|
||||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||||
const encryptService = mock<EncryptService>();
|
const encryptService = mock<EncryptService>();
|
||||||
|
@ -46,6 +48,7 @@ describe("electronCryptoService", () => {
|
||||||
stateProvider = new FakeStateProvider(accountService);
|
stateProvider = new FakeStateProvider(accountService);
|
||||||
|
|
||||||
sut = new ElectronCryptoService(
|
sut = new ElectronCryptoService(
|
||||||
|
pinService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
keyGenerationService,
|
keyGenerationService,
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
|
@ -22,6 +23,7 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
export class ElectronCryptoService extends CryptoService {
|
export class ElectronCryptoService extends CryptoService {
|
||||||
constructor(
|
constructor(
|
||||||
|
pinService: PinServiceAbstraction,
|
||||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
keyGenerationService: KeyGenerationService,
|
keyGenerationService: KeyGenerationService,
|
||||||
cryptoFunctionService: CryptoFunctionService,
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
|
@ -35,6 +37,7 @@ export class ElectronCryptoService extends CryptoService {
|
||||||
kdfConfigService: KdfConfigService,
|
kdfConfigService: KdfConfigService,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
|
pinService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
keyGenerationService,
|
keyGenerationService,
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
|
@ -174,7 +177,10 @@ export class ElectronCryptoService extends CryptoService {
|
||||||
if (!encUserKey) {
|
if (!encUserKey) {
|
||||||
throw new Error("No user key found during biometric migration");
|
throw new Error("No user key found during biometric migration");
|
||||||
}
|
}
|
||||||
const userKey = await this.decryptUserKeyWithMasterKey(masterKey, encUserKey);
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||||
|
masterKey,
|
||||||
|
encUserKey,
|
||||||
|
);
|
||||||
// migrate
|
// migrate
|
||||||
await this.storeBiometricKey(userKey, userId);
|
await this.storeBiometricKey(userKey, userId);
|
||||||
await this.stateService.setCryptoMasterKeyBiometric(null, { userId });
|
await this.stateService.setCryptoMasterKeyBiometric(null, { userId });
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
import { concatMap, takeUntil, map, lastValueFrom } from "rxjs";
|
import { concatMap, takeUntil, map } from "rxjs";
|
||||||
import { tap } from "rxjs/operators";
|
import { tap } from "rxjs/operators";
|
||||||
|
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
|
@ -16,7 +16,6 @@ import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
|
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
|
||||||
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
|
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
|
||||||
import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-verify.component";
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-two-factor-setup",
|
selector: "app-two-factor-setup",
|
||||||
|
@ -66,17 +65,17 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
|
||||||
async manage(type: TwoFactorProviderType) {
|
async manage(type: TwoFactorProviderType) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TwoFactorProviderType.OrganizationDuo: {
|
case TwoFactorProviderType.OrganizationDuo: {
|
||||||
const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, {
|
const result: AuthResponse<TwoFactorDuoResponse> = await this.callTwoFactorVerifyDialog(
|
||||||
data: { type: type, organizationId: this.organizationId },
|
TwoFactorProviderType.OrganizationDuo,
|
||||||
});
|
|
||||||
const result: AuthResponse<TwoFactorDuoResponse> = await lastValueFrom(
|
|
||||||
twoFactorVerifyDialogRef.closed,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
|
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
|
||||||
|
duoComp.type = TwoFactorProviderType.OrganizationDuo;
|
||||||
|
duoComp.organizationId = this.organizationId;
|
||||||
duoComp.auth(result);
|
duoComp.auth(result);
|
||||||
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||||
this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo);
|
this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo);
|
||||||
|
|
|
@ -2,7 +2,17 @@ import { DOCUMENT } from "@angular/common";
|
||||||
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { NavigationEnd, Router } from "@angular/router";
|
import { NavigationEnd, Router } from "@angular/router";
|
||||||
import * as jq from "jquery";
|
import * as jq from "jquery";
|
||||||
import { Subject, filter, firstValueFrom, map, switchMap, takeUntil, timeout, timer } from "rxjs";
|
import {
|
||||||
|
Subject,
|
||||||
|
combineLatest,
|
||||||
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
|
map,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
timeout,
|
||||||
|
timer,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { LogoutReason } from "@bitwarden/auth/common";
|
import { LogoutReason } from "@bitwarden/auth/common";
|
||||||
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
|
||||||
|
@ -16,6 +26,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
||||||
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
@ -242,8 +253,12 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||||
new SendOptionsPolicy(),
|
new SendOptionsPolicy(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
this.paymentMethodWarningsRefresh$
|
combineLatest([
|
||||||
|
this.configService.getFeatureFlag$(FeatureFlag.ShowPaymentMethodWarningBanners),
|
||||||
|
this.paymentMethodWarningsRefresh$,
|
||||||
|
])
|
||||||
.pipe(
|
.pipe(
|
||||||
|
filter(([showPaymentMethodWarningBanners]) => showPaymentMethodWarningBanners),
|
||||||
switchMap(() => this.organizationService.memberOrganizations$),
|
switchMap(() => this.organizationService.memberOrganizations$),
|
||||||
switchMap(
|
switchMap(
|
||||||
async (organizations) =>
|
async (organizations) =>
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
@ -50,6 +51,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
private keyRotationService: UserKeyRotationService,
|
private keyRotationService: UserKeyRotationService,
|
||||||
kdfConfigService: KdfConfigService,
|
kdfConfigService: KdfConfigService,
|
||||||
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
i18nService,
|
i18nService,
|
||||||
|
@ -61,6 +63,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||||
stateService,
|
stateService,
|
||||||
dialogService,
|
dialogService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
|
masterPasswordService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +168,22 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
||||||
newMasterKey: MasterKey,
|
newMasterKey: MasterKey,
|
||||||
newUserKey: [UserKey, EncString],
|
newUserKey: [UserKey, EncString],
|
||||||
) {
|
) {
|
||||||
const masterKey = await this.cryptoService.getOrDeriveMasterKey(this.currentMasterPassword);
|
const masterKey = await this.cryptoService.makeMasterKey(
|
||||||
|
this.currentMasterPassword,
|
||||||
|
await this.stateService.getEmail(),
|
||||||
|
await this.kdfConfigService.getKdfConfig(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
|
if (userKey == null) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"error",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("invalidMasterPassword"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const request = new PasswordRequest();
|
const request = new PasswordRequest();
|
||||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||||
this.currentMasterPassword,
|
this.currentMasterPassword,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { takeUntil } from "rxjs";
|
||||||
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
@ -60,6 +61,7 @@ export class EmergencyAccessTakeoverComponent
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>,
|
private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>,
|
||||||
kdfConfigService: KdfConfigService,
|
kdfConfigService: KdfConfigService,
|
||||||
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
i18nService,
|
i18nService,
|
||||||
|
@ -71,6 +73,7 @@ export class EmergencyAccessTakeoverComponent
|
||||||
stateService,
|
stateService,
|
||||||
dialogService,
|
dialogService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
|
masterPasswordService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitward
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
@ -34,6 +35,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
|
||||||
userVerificationService: UserVerificationService,
|
userVerificationService: UserVerificationService,
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
kdfConfigService: KdfConfigService,
|
kdfConfigService: KdfConfigService,
|
||||||
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
router,
|
router,
|
||||||
|
@ -49,6 +51,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
|
||||||
logService,
|
logService,
|
||||||
dialogService,
|
dialogService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
|
masterPasswordService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,129 +1,143 @@
|
||||||
<div *ngIf="selfHosted" class="page-header">
|
<bit-section>
|
||||||
<h1>{{ "subscription" | i18n }}</h1>
|
<h2 *ngIf="!selfHosted" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||||
</div>
|
<bit-callout
|
||||||
<div *ngIf="!selfHosted" class="tabbed-header">
|
type="info"
|
||||||
<h1>{{ "goPremium" | i18n }}</h1>
|
*ngIf="canAccessPremium$ | async"
|
||||||
</div>
|
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||||
<bit-callout
|
icon="bwi bwi-star-f"
|
||||||
type="info"
|
|
||||||
*ngIf="canAccessPremium$ | async"
|
|
||||||
title="{{ 'youHavePremiumAccess' | i18n }}"
|
|
||||||
icon="bwi bwi-star-f"
|
|
||||||
>
|
|
||||||
{{ "alreadyPremiumFromOrg" | i18n }}
|
|
||||||
</bit-callout>
|
|
||||||
<bit-callout type="success">
|
|
||||||
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
|
||||||
<ul class="bwi-ul">
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpStorage" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpEmergency" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpReports" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpTotp" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpSupport" | i18n }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
|
|
||||||
{{ "premiumSignUpFuture" | i18n }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p class="text-lg" [ngClass]="{ 'mb-0': !selfHosted }">
|
|
||||||
{{
|
|
||||||
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
|
||||||
}}
|
|
||||||
<a routerLink="/create-organization" [queryParams]="{ plan: 'families' }">{{
|
|
||||||
"bitwardenFamiliesPlan" | i18n
|
|
||||||
}}</a>
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
bitButton
|
|
||||||
href="{{ this.cloudWebVaultUrl }}/#/settings/subscription/premium"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
buttonType="secondary"
|
|
||||||
*ngIf="selfHosted"
|
|
||||||
>
|
>
|
||||||
{{ "purchasePremium" | i18n }}
|
{{ "alreadyPremiumFromOrg" | i18n }}
|
||||||
</a>
|
</bit-callout>
|
||||||
</bit-callout>
|
<bit-callout type="success">
|
||||||
<ng-container *ngIf="selfHosted">
|
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
|
||||||
<p>{{ "uploadLicenseFilePremium" | i18n }}</p>
|
<ul class="bwi-ul">
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<li>
|
||||||
<div class="form-group">
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
<label for="file">{{ "licenseFile" | i18n }}</label>
|
{{ "premiumSignUpStorage" | i18n }}
|
||||||
<input type="file" id="file" class="form-control-file" name="file" required />
|
</li>
|
||||||
<small class="form-text text-muted">{{
|
<li>
|
||||||
"licenseFileDesc" | i18n: "bitwarden_premium_license.json"
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
}}</small>
|
{{ "premiumSignUpTwoStepOptions" | i18n }}
|
||||||
</div>
|
</li>
|
||||||
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpEmergency" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpReports" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpTotp" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpSupport" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||||
|
{{ "premiumSignUpFuture" | i18n }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !selfHosted }">
|
||||||
|
{{
|
||||||
|
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
||||||
|
}}
|
||||||
|
<a
|
||||||
|
bitLink
|
||||||
|
linkType="primary"
|
||||||
|
routerLink="/create-organization"
|
||||||
|
[queryParams]="{ plan: 'families' }"
|
||||||
|
>{{ "bitwardenFamiliesPlan" | i18n }}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
bitButton
|
||||||
|
href="{{ this.cloudWebVaultUrl }}/#/settings/subscription/premium"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
buttonType="secondary"
|
||||||
|
*ngIf="selfHosted"
|
||||||
|
>
|
||||||
|
{{ "purchasePremium" | i18n }}
|
||||||
|
</a>
|
||||||
|
</bit-callout>
|
||||||
|
</bit-section>
|
||||||
|
<bit-section *ngIf="selfHosted">
|
||||||
|
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||||
|
<form [formGroup]="licenseForm" [bitSubmit]="submit">
|
||||||
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||||
|
<div>
|
||||||
|
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||||
|
{{ "chooseFile" | i18n }}
|
||||||
|
</button>
|
||||||
|
{{ this.licenseFile ? this.licenseFile.name : ("noFileChosen" | i18n) }}
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
bitInput
|
||||||
|
#fileSelector
|
||||||
|
type="file"
|
||||||
|
formControlName="file"
|
||||||
|
(change)="setSelectedFile($event)"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<button type="submit" buttonType="primary" bitButton bitFormButton>
|
||||||
{{ "submit" | i18n }}
|
{{ "submit" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</ng-container>
|
</bit-section>
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
|
<form [formGroup]="addonForm" [bitSubmit]="submit" *ngIf="!selfHosted">
|
||||||
<h2 class="mt-5">{{ "addons" | i18n }}</h2>
|
<bit-section>
|
||||||
<div class="row">
|
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||||
<div class="form-group col-6">
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
<label for="additionalStorage">{{ "additionalStorageGb" | i18n }}</label>
|
<bit-form-field class="tw-col-span-6">
|
||||||
<input
|
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||||
id="additionalStorage"
|
<input
|
||||||
class="form-control"
|
bitInput
|
||||||
type="number"
|
formControlName="additionalStorage"
|
||||||
name="AdditionalStorageGb"
|
type="number"
|
||||||
[(ngModel)]="additionalStorage"
|
step="1"
|
||||||
min="0"
|
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||||
max="99"
|
/>
|
||||||
step="1"
|
<bit-hint>{{
|
||||||
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
"additionalStorageIntervalDesc"
|
||||||
/>
|
| i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n)
|
||||||
<small class="text-muted form-text">{{
|
}}</bit-hint>
|
||||||
"additionalStorageIntervalDesc"
|
</bit-form-field>
|
||||||
| i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n)
|
|
||||||
}}</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</bit-section>
|
||||||
<h2 class="spaced-header">{{ "summary" | i18n }}</h2>
|
<bit-section>
|
||||||
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||||
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB ×
|
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
||||||
{{ storageGbPrice | currency: "$" }} =
|
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB ×
|
||||||
{{ additionalStorageTotal | currency: "$" }}
|
{{ storageGbPrice | currency: "$" }} =
|
||||||
<hr class="my-3" />
|
{{ additionalStorageTotal | currency: "$" }}
|
||||||
<h2 class="spaced-header mb-4">{{ "paymentInformation" | i18n }}</h2>
|
<hr class="tw-my-3" />
|
||||||
<app-payment [hideBank]="true"></app-payment>
|
</bit-section>
|
||||||
<app-tax-info></app-tax-info>
|
<bit-section>
|
||||||
<div id="price" class="my-4">
|
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
||||||
<div class="text-muted text-sm">
|
<app-payment [hideBank]="true"></app-payment>
|
||||||
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}
|
<app-tax-info></app-tax-info>
|
||||||
<br />
|
<div id="price" class="tw-my-4">
|
||||||
<ng-container>
|
<div class="tw-text-muted tw-text-sm">
|
||||||
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
|
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}
|
||||||
</ng-container>
|
<br />
|
||||||
|
<ng-container>
|
||||||
|
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
<hr class="tw-my-1 tw-w-1/4 tw-ml-0" />
|
||||||
|
<p bitTypography="body1">
|
||||||
|
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-1 col-3 ml-0" />
|
<p bitTypography="body2">{{ "paymentChargedAnnually" | i18n }}</p>
|
||||||
<p class="text-lg">
|
<button type="submit" bitButton bitFormButton>
|
||||||
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
|
{{ "submit" | i18n }}
|
||||||
</p>
|
</button>
|
||||||
</div>
|
</bit-section>
|
||||||
<small class="text-muted font-italic">{{ "paymentChargedAnnually" | i18n }}</small>
|
|
||||||
<button type="submit" bitButton [loading]="form.loading">
|
|
||||||
{{ "submit" | i18n }}
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { Component, OnInit, ViewChild } from "@angular/core";
|
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||||
|
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { firstValueFrom, Observable } from "rxjs";
|
import { firstValueFrom, Observable } from "rxjs";
|
||||||
|
|
||||||
|
@ -7,7 +8,6 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
|
||||||
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
@ -26,11 +26,16 @@ export class PremiumComponent implements OnInit {
|
||||||
premiumPrice = 10;
|
premiumPrice = 10;
|
||||||
familyPlanMaxUserCount = 6;
|
familyPlanMaxUserCount = 6;
|
||||||
storageGbPrice = 4;
|
storageGbPrice = 4;
|
||||||
additionalStorage = 0;
|
|
||||||
cloudWebVaultUrl: string;
|
cloudWebVaultUrl: string;
|
||||||
|
licenseFile: File = null;
|
||||||
|
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
|
protected licenseForm = new FormGroup({
|
||||||
|
file: new FormControl(null, [Validators.required]),
|
||||||
|
});
|
||||||
|
protected addonForm = new FormGroup({
|
||||||
|
additionalStorage: new FormControl(0, [Validators.max(99), Validators.min(0)]),
|
||||||
|
});
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
|
@ -39,14 +44,17 @@ export class PremiumComponent implements OnInit {
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private syncService: SyncService,
|
private syncService: SyncService,
|
||||||
private logService: LogService,
|
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||||
) {
|
) {
|
||||||
this.selfHosted = platformUtilsService.isSelfHost();
|
this.selfHosted = platformUtilsService.isSelfHost();
|
||||||
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
|
||||||
}
|
}
|
||||||
|
protected setSelectedFile(event: Event) {
|
||||||
|
const fileInputEl = <HTMLInputElement>event.target;
|
||||||
|
const file: File = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
|
||||||
|
this.licenseFile = file;
|
||||||
|
}
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
||||||
if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) {
|
if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) {
|
||||||
|
@ -56,13 +64,11 @@ export class PremiumComponent implements OnInit {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
submit = async () => {
|
||||||
async submit() {
|
this.licenseForm.markAllAsTouched();
|
||||||
let files: FileList = null;
|
this.addonForm.markAllAsTouched();
|
||||||
if (this.selfHosted) {
|
if (this.selfHosted) {
|
||||||
const fileEl = document.getElementById("file") as HTMLInputElement;
|
if (this.licenseFile == null) {
|
||||||
files = fileEl.files;
|
|
||||||
if (files == null || files.length === 0) {
|
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
this.i18nService.t("errorOccurred"),
|
this.i18nService.t("errorOccurred"),
|
||||||
|
@ -72,53 +78,48 @@ export class PremiumComponent implements OnInit {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (this.selfHosted) {
|
||||||
if (this.selfHosted) {
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
if (!this.tokenService.getEmailVerified()) {
|
||||||
if (!this.tokenService.getEmailVerified()) {
|
this.platformUtilsService.showToast(
|
||||||
this.platformUtilsService.showToast(
|
"error",
|
||||||
"error",
|
this.i18nService.t("errorOccurred"),
|
||||||
this.i18nService.t("errorOccurred"),
|
this.i18nService.t("verifyEmailFirst"),
|
||||||
this.i18nService.t("verifyEmailFirst"),
|
);
|
||||||
);
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append("license", files[0]);
|
|
||||||
this.formPromise = this.apiService.postAccountLicense(fd).then(() => {
|
|
||||||
return this.finalizePremium();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.formPromise = this.paymentComponent
|
|
||||||
.createPaymentToken()
|
|
||||||
.then((result) => {
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append("paymentMethodType", result[1].toString());
|
|
||||||
if (result[0] != null) {
|
|
||||||
fd.append("paymentToken", result[0]);
|
|
||||||
}
|
|
||||||
fd.append("additionalStorageGb", (this.additionalStorage || 0).toString());
|
|
||||||
fd.append("country", this.taxInfoComponent.taxInfo.country);
|
|
||||||
fd.append("postalCode", this.taxInfoComponent.taxInfo.postalCode);
|
|
||||||
return this.apiService.postPremium(fd);
|
|
||||||
})
|
|
||||||
.then((paymentResponse) => {
|
|
||||||
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {
|
|
||||||
return this.paymentComponent.handleStripeCardPayment(
|
|
||||||
paymentResponse.paymentIntentClientSecret,
|
|
||||||
() => this.finalizePremium(),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return this.finalizePremium();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
await this.formPromise;
|
|
||||||
} catch (e) {
|
const fd = new FormData();
|
||||||
this.logService.error(e);
|
fd.append("license", this.licenseFile);
|
||||||
|
await this.apiService.postAccountLicense(fd).then(() => {
|
||||||
|
return this.finalizePremium();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.paymentComponent
|
||||||
|
.createPaymentToken()
|
||||||
|
.then((result) => {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("paymentMethodType", result[1].toString());
|
||||||
|
if (result[0] != null) {
|
||||||
|
fd.append("paymentToken", result[0]);
|
||||||
|
}
|
||||||
|
fd.append("additionalStorageGb", (this.additionalStorage || 0).toString());
|
||||||
|
fd.append("country", this.taxInfoComponent.taxInfo.country);
|
||||||
|
fd.append("postalCode", this.taxInfoComponent.taxInfo.postalCode);
|
||||||
|
return this.apiService.postPremium(fd);
|
||||||
|
})
|
||||||
|
.then((paymentResponse) => {
|
||||||
|
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {
|
||||||
|
return this.paymentComponent.handleStripeCardPayment(
|
||||||
|
paymentResponse.paymentIntentClientSecret,
|
||||||
|
() => this.finalizePremium(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.finalizePremium();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async finalizePremium() {
|
async finalizePremium() {
|
||||||
await this.apiService.refreshIdentityToken();
|
await this.apiService.refreshIdentityToken();
|
||||||
|
@ -127,6 +128,9 @@ export class PremiumComponent implements OnInit {
|
||||||
await this.router.navigate(["/settings/subscription/user-subscription"]);
|
await this.router.navigate(["/settings/subscription/user-subscription"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get additionalStorage(): number {
|
||||||
|
return this.addonForm.get("additionalStorage").value;
|
||||||
|
}
|
||||||
get additionalStorageTotal(): number {
|
get additionalStorageTotal(): number {
|
||||||
return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
|
return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,65 +1,57 @@
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<form [formGroup]="adjustSubscriptionForm" [bitSubmit]="submit">
|
||||||
<div>
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
<div class="row">
|
<div class="tw-col-span-8">
|
||||||
<div class="form-group col-8">
|
<bit-form-field>
|
||||||
<label for="newSeatCount">{{ "subscriptionSeats" | i18n }}</label>
|
<bit-label>{{ "subscriptionSeats" | i18n }}</bit-label>
|
||||||
<input
|
<input bitInput formControlName="newSeatCount" type="number" min="0" step="1" />
|
||||||
id="newSeatCount"
|
<bit-hint>
|
||||||
class="form-control"
|
|
||||||
type="number"
|
|
||||||
name="NewSeatCount"
|
|
||||||
[(ngModel)]="newSeatCount"
|
|
||||||
min="0"
|
|
||||||
step="1"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<small class="d-block text-muted mb-4">
|
|
||||||
<strong>{{ "total" | i18n }}:</strong> {{ additionalSeatCount || 0 }} ×
|
<strong>{{ "total" | i18n }}:</strong> {{ additionalSeatCount || 0 }} ×
|
||||||
{{ seatPrice | currency: "$" }} = {{ adjustedSeatTotal | currency: "$" }} /
|
{{ seatPrice | currency: "$" }} = {{ adjustedSeatTotal | currency: "$" }} /
|
||||||
{{ interval | i18n }}
|
{{ interval | i18n }}</bit-hint
|
||||||
</small>
|
>
|
||||||
</div>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mb-4">
|
</div>
|
||||||
<div class="form-group col-sm">
|
<div>
|
||||||
<div class="form-check">
|
<bit-form-control>
|
||||||
<input
|
<input
|
||||||
id="limitSubscription"
|
bitCheckbox
|
||||||
class="form-check-input"
|
formControlName="limitSubscription"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="LimitSubscription"
|
(change)="limitSubscriptionChanged()"
|
||||||
[(ngModel)]="limitSubscription"
|
/>
|
||||||
(change)="limitSubscriptionChanged()"
|
<bit-label>{{ "limitSubscription" | i18n }}</bit-label>
|
||||||
/>
|
<bit-hint> {{ "limitSubscriptionDesc" | i18n }}</bit-hint>
|
||||||
<label for="limitSubscription">{{ "limitSubscription" | i18n }}</label>
|
</bit-form-control>
|
||||||
</div>
|
</div>
|
||||||
<small class="d-block text-muted">{{ "limitSubscriptionDesc" | i18n }}</small>
|
<div
|
||||||
</div>
|
class="tw-grid tw-grid-cols-12 tw-gap-4 tw-mb-4"
|
||||||
</div>
|
[hidden]="!adjustSubscriptionForm.value.limitSubscription"
|
||||||
<div class="row mb-4" [hidden]="!limitSubscription">
|
>
|
||||||
<div class="form-group col-sm">
|
<div class="tw-col-span-8">
|
||||||
<label for="maxAutoscaleSeats">{{ "maxSeatLimit" | i18n }}</label>
|
<bit-form-field>
|
||||||
|
<bit-label>{{ "maxSeatLimit" | i18n }}</bit-label>
|
||||||
<input
|
<input
|
||||||
id="maxAutoscaleSeats"
|
bitInput
|
||||||
class="form-control col-8"
|
formControlName="newMaxSeats"
|
||||||
type="number"
|
type="number"
|
||||||
name="MaxAutoscaleSeats"
|
[min]="
|
||||||
[(ngModel)]="newMaxSeats"
|
adjustSubscriptionForm.value.newSeatCount == null
|
||||||
[min]="newSeatCount == null ? 1 : newSeatCount"
|
? 1
|
||||||
|
: adjustSubscriptionForm.value.newSeatCount
|
||||||
|
"
|
||||||
step="1"
|
step="1"
|
||||||
[required]="limitSubscription"
|
|
||||||
/>
|
/>
|
||||||
<small class="d-block text-muted">
|
<bit-hint>
|
||||||
<strong>{{ "maxSeatCost" | i18n }}:</strong> {{ additionalMaxSeatCount || 0 }} ×
|
<strong>{{ "maxSeatCost" | i18n }}:</strong> {{ additionalMaxSeatCount || 0 }} ×
|
||||||
{{ seatPrice | currency: "$" }} = {{ maxSeatTotal | currency: "$" }} /
|
{{ seatPrice | currency: "$" }} = {{ maxSeatTotal | currency: "$" }} /
|
||||||
{{ interval | i18n }}
|
{{ interval | i18n }}</bit-hint
|
||||||
</small>
|
>
|
||||||
</div>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
||||||
<span>{{ "save" | i18n }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button bitButton buttonType="primary" bitFormButton type="submit">
|
||||||
|
{{ "save" | i18n }}
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<app-payment [showMethods]="false"></app-payment>
|
<app-payment [showMethods]="false"></app-payment>
|
||||||
|
|
|
@ -1,77 +1,102 @@
|
||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||||
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request";
|
import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-adjust-subscription",
|
selector: "app-adjust-subscription",
|
||||||
templateUrl: "adjust-subscription.component.html",
|
templateUrl: "adjust-subscription.component.html",
|
||||||
})
|
})
|
||||||
export class AdjustSubscription {
|
export class AdjustSubscription implements OnInit, OnDestroy {
|
||||||
@Input() organizationId: string;
|
@Input() organizationId: string;
|
||||||
@Input() maxAutoscaleSeats: number;
|
@Input() maxAutoscaleSeats: number;
|
||||||
@Input() currentSeatCount: number;
|
@Input() currentSeatCount: number;
|
||||||
@Input() seatPrice = 0;
|
@Input() seatPrice = 0;
|
||||||
@Input() interval = "year";
|
@Input() interval = "year";
|
||||||
@Output() onAdjusted = new EventEmitter();
|
@Output() onAdjusted = new EventEmitter();
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
formPromise: Promise<void>;
|
adjustSubscriptionForm = this.formBuilder.group({
|
||||||
limitSubscription: boolean;
|
newSeatCount: [0, [Validators.min(0)]],
|
||||||
newSeatCount: number;
|
limitSubscription: [false],
|
||||||
newMaxSeats: number;
|
newMaxSeats: [0, [Validators.min(0)]],
|
||||||
|
});
|
||||||
|
get limitSubscription(): boolean {
|
||||||
|
return this.adjustSubscriptionForm.value.limitSubscription;
|
||||||
|
}
|
||||||
constructor(
|
constructor(
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private logService: LogService,
|
|
||||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
|
private formBuilder: FormBuilder,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.limitSubscription = this.maxAutoscaleSeats != null;
|
this.adjustSubscriptionForm.patchValue({
|
||||||
this.newSeatCount = this.currentSeatCount;
|
newSeatCount: this.currentSeatCount,
|
||||||
this.newMaxSeats = this.maxAutoscaleSeats;
|
limitSubscription: this.maxAutoscaleSeats != null,
|
||||||
|
newMaxSeats: this.maxAutoscaleSeats,
|
||||||
|
});
|
||||||
|
this.adjustSubscriptionForm
|
||||||
|
.get("limitSubscription")
|
||||||
|
.valueChanges.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((value: boolean) => {
|
||||||
|
if (value) {
|
||||||
|
this.adjustSubscriptionForm
|
||||||
|
.get("newMaxSeats")
|
||||||
|
.addValidators([
|
||||||
|
Validators.min(
|
||||||
|
this.adjustSubscriptionForm.value.newSeatCount == null
|
||||||
|
? 1
|
||||||
|
: this.adjustSubscriptionForm.value.newSeatCount,
|
||||||
|
),
|
||||||
|
Validators.required,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
this.adjustSubscriptionForm.get("newMaxSeats").updateValueAndValidity();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
ngOnDestroy() {
|
||||||
try {
|
this.destroy$.next();
|
||||||
const request = new OrganizationSubscriptionUpdateRequest(
|
this.destroy$.complete();
|
||||||
this.additionalSeatCount,
|
}
|
||||||
this.newMaxSeats,
|
submit = async () => {
|
||||||
);
|
this.adjustSubscriptionForm.markAllAsTouched();
|
||||||
this.formPromise = this.organizationApiService.updatePasswordManagerSeats(
|
if (this.adjustSubscriptionForm.invalid) {
|
||||||
this.organizationId,
|
return;
|
||||||
request,
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.formPromise;
|
|
||||||
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("subscriptionUpdated"),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
}
|
||||||
|
const request = new OrganizationSubscriptionUpdateRequest(
|
||||||
|
this.additionalSeatCount,
|
||||||
|
this.adjustSubscriptionForm.value.newMaxSeats,
|
||||||
|
);
|
||||||
|
await this.organizationApiService.updatePasswordManagerSeats(this.organizationId, request);
|
||||||
|
|
||||||
|
this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated"));
|
||||||
|
|
||||||
this.onAdjusted.emit();
|
this.onAdjusted.emit();
|
||||||
}
|
};
|
||||||
|
|
||||||
limitSubscriptionChanged() {
|
limitSubscriptionChanged() {
|
||||||
if (!this.limitSubscription) {
|
if (!this.adjustSubscriptionForm.value.limitSubscription) {
|
||||||
this.newMaxSeats = null;
|
this.adjustSubscriptionForm.value.newMaxSeats = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get additionalSeatCount(): number {
|
get additionalSeatCount(): number {
|
||||||
return this.newSeatCount ? this.newSeatCount - this.currentSeatCount : 0;
|
return this.adjustSubscriptionForm.value.newSeatCount
|
||||||
|
? this.adjustSubscriptionForm.value.newSeatCount - this.currentSeatCount
|
||||||
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get additionalMaxSeatCount(): number {
|
get additionalMaxSeatCount(): number {
|
||||||
return this.newMaxSeats ? this.newMaxSeats - this.currentSeatCount : 0;
|
return this.adjustSubscriptionForm.value.newMaxSeats
|
||||||
|
? this.adjustSubscriptionForm.value.newMaxSeats - this.currentSeatCount
|
||||||
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
get adjustedSeatTotal(): number {
|
get adjustedSeatTotal(): number {
|
||||||
|
|
|
@ -1,400 +1,428 @@
|
||||||
<ng-container *ngIf="loading">
|
<ng-container *ngIf="loading">
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||||
title="{{ 'loading' | i18n }}"
|
title="{{ 'loading' | i18n }}"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="createOrganization && selfHosted">
|
<ng-container *ngIf="createOrganization && selfHosted">
|
||||||
<p>{{ "uploadLicenseFileOrg" | i18n }}</p>
|
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
|
||||||
<div class="form-group">
|
<bit-form-field>
|
||||||
<label for="file">{{ "licenseFile" | i18n }}</label>
|
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||||
<input type="file" id="file" class="form-control-file" name="file" required />
|
<div>
|
||||||
<small class="form-text text-muted">{{
|
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||||
"licenseFileDesc" | i18n: "bitwarden_organization_license.json"
|
{{ "chooseFile" | i18n }}
|
||||||
}}</small>
|
</button>
|
||||||
</div>
|
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
</div>
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
<input
|
||||||
<span>{{ "submit" | i18n }}</span>
|
#fileSelector
|
||||||
|
hidden
|
||||||
|
bitInput
|
||||||
|
type="file"
|
||||||
|
formControlName="file"
|
||||||
|
(change)="setSelectedFile($event)"
|
||||||
|
accept="application/JSON"
|
||||||
|
/>
|
||||||
|
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
|
<button type="submit" bitButton bitFormButton buttonType="primary">
|
||||||
|
{{ "submit" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<form
|
<form
|
||||||
#form
|
|
||||||
[formGroup]="formGroup"
|
[formGroup]="formGroup"
|
||||||
(ngSubmit)="submit()"
|
[bitSubmit]="submit"
|
||||||
[appApiAction]="formPromise"
|
|
||||||
ngNativeValidate
|
|
||||||
*ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans"
|
*ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans"
|
||||||
class="tw-pt-6"
|
class="tw-pt-6"
|
||||||
>
|
>
|
||||||
<app-org-info
|
<bit-section>
|
||||||
(changedBusinessOwned)="changedOwnedBusiness()"
|
<app-org-info
|
||||||
[formGroup]="formGroup"
|
(changedBusinessOwned)="changedOwnedBusiness()"
|
||||||
[createOrganization]="createOrganization"
|
[formGroup]="formGroup"
|
||||||
[isProvider]="!!providerId"
|
[createOrganization]="createOrganization"
|
||||||
[acceptingSponsorship]="acceptingSponsorship"
|
[isProvider]="!!providerId"
|
||||||
></app-org-info>
|
[acceptingSponsorship]="acceptingSponsorship"
|
||||||
<h2 class="mt-5">{{ "chooseYourPlan" | i18n }}</h2>
|
>
|
||||||
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block">
|
</app-org-info>
|
||||||
<input
|
</bit-section>
|
||||||
class="form-check-input"
|
<bit-section>
|
||||||
type="radio"
|
<h2 bitTypography="h2">{{ "chooseYourPlan" | i18n }}</h2>
|
||||||
name="product"
|
<div *ngFor="let selectableProduct of selectableProducts">
|
||||||
id="product{{ selectableProduct.product }}"
|
<bit-radio-group formControlName="product" [block]="true">
|
||||||
[value]="selectableProduct.product"
|
<bit-radio-button [value]="selectableProduct.product" (change)="changedProduct()">
|
||||||
formControlName="product"
|
<bit-label>{{ selectableProduct.nameLocalizationKey | i18n }}</bit-label>
|
||||||
(change)="changedProduct()"
|
<bit-hint class="tw-text-sm"
|
||||||
/>
|
>{{ selectableProduct.descriptionLocalizationKey | i18n: "1" }}
|
||||||
<label class="form-check-label" for="product{{ selectableProduct.product }}">
|
<ng-container
|
||||||
{{ selectableProduct.nameLocalizationKey | i18n }}
|
*ngIf="selectableProduct.product === productTypes.Enterprise; else nonEnterprisePlans"
|
||||||
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n: "1" }}</small>
|
>
|
||||||
<ng-container
|
<ul class="tw-pl-0 tw-list-inside tw-mb-0">
|
||||||
*ngIf="selectableProduct.product === productTypes.Enterprise; else nonEnterprisePlans"
|
<li>{{ "includeAllTeamsFeatures" | i18n }}</li>
|
||||||
>
|
<li *ngIf="selectableProduct.hasSelfHost">{{ "onPremHostingOptional" | i18n }}</li>
|
||||||
<small>• {{ "includeAllTeamsFeatures" | i18n }}</small>
|
<li *ngIf="selectableProduct.hasSso">{{ "includeSsoAuthentication" | i18n }}</li>
|
||||||
<small *ngIf="selectableProduct.hasSelfHost">• {{ "onPremHostingOptional" | i18n }}</small>
|
<li *ngIf="selectableProduct.hasPolicies">
|
||||||
<small *ngIf="selectableProduct.hasSso">• {{ "includeSsoAuthentication" | i18n }}</small>
|
{{ "includeEnterprisePolicies" | i18n }}
|
||||||
<small *ngIf="selectableProduct.hasPolicies"
|
</li>
|
||||||
>• {{ "includeEnterprisePolicies" | i18n }}</small
|
<li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
||||||
>
|
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
||||||
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization"
|
</li>
|
||||||
>•
|
</ul>
|
||||||
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
</ng-container>
|
||||||
</small>
|
<ng-template #nonEnterprisePlans>
|
||||||
</ng-container>
|
<ng-container
|
||||||
<ng-template #nonEnterprisePlans>
|
*ngIf="selectableProduct.product === productTypes.Teams; else fullFeatureList"
|
||||||
<ng-container
|
>
|
||||||
*ngIf="selectableProduct.product === productTypes.Teams; else fullFeatureList"
|
<ul class="tw-pl-0 tw-list-inside tw-mb-0">
|
||||||
>
|
<li>{{ "includeAllTeamsStarterFeatures" | i18n }}</li>
|
||||||
<small>• {{ "includeAllTeamsStarterFeatures" | i18n }}</small>
|
<li>{{ "chooseMonthlyOrAnnualBilling" | i18n }}</li>
|
||||||
<small>• {{ "chooseMonthlyOrAnnualBilling" | i18n }}</small>
|
<li>{{ "abilityToAddMoreThanNMembers" | i18n: 10 }}</li>
|
||||||
<small>• {{ "abilityToAddMoreThanNMembers" | i18n: 10 }}</small>
|
<li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
||||||
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
||||||
• {{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
</li>
|
||||||
</small>
|
</ul>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #fullFeatureList>
|
<ng-template #fullFeatureList>
|
||||||
<small *ngIf="selectableProduct.product == productTypes.Free"
|
<ul class="tw-pl-0 tw-list-inside tw-mb-0">
|
||||||
>• {{ "limitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}</small
|
<li *ngIf="selectableProduct.product == productTypes.Free">
|
||||||
|
{{ "limitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
*ngIf="
|
||||||
|
selectableProduct.product != productTypes.Free &&
|
||||||
|
selectableProduct.product != productTypes.TeamsStarter &&
|
||||||
|
selectableProduct.PasswordManager.maxSeats
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ "addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="!selectableProduct.PasswordManager.maxSeats">
|
||||||
|
{{ "addShareUnlimitedUsers" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.PasswordManager.maxCollections">
|
||||||
|
{{
|
||||||
|
"limitedCollections" | i18n: selectableProduct.PasswordManager.maxCollections
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.PasswordManager.maxAdditionalSeats">
|
||||||
|
{{
|
||||||
|
"addShareLimitedUsers"
|
||||||
|
| i18n: selectableProduct.PasswordManager.maxAdditionalSeats
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="!selectableProduct.PasswordManager.maxCollections">
|
||||||
|
{{ "createUnlimitedCollections" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.PasswordManager.baseStorageGb">
|
||||||
|
{{
|
||||||
|
"gbEncryptedFileStorage"
|
||||||
|
| i18n: selectableProduct.PasswordManager.baseStorageGb + "GB"
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.hasGroups">
|
||||||
|
{{ "controlAccessWithGroups" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.hasApi">{{ "trackAuditLogs" | i18n }}</li>
|
||||||
|
<li *ngIf="selectableProduct.hasDirectory">
|
||||||
|
{{ "syncUsersFromDirectory" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.hasSelfHost">
|
||||||
|
{{ "onPremHostingOptional" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.usersGetPremium">{{ "usersGetPremium" | i18n }}</li>
|
||||||
|
<li *ngIf="selectableProduct.product != productTypes.Free">
|
||||||
|
{{ "priorityCustomerSupport" | i18n }}
|
||||||
|
</li>
|
||||||
|
<li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
||||||
|
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ng-template>
|
||||||
|
</ng-template>
|
||||||
|
</bit-hint>
|
||||||
|
</bit-radio-button>
|
||||||
|
<span *ngIf="selectableProduct.product != productTypes.Free">
|
||||||
|
<ng-container
|
||||||
|
*ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship"
|
||||||
>
|
>
|
||||||
<small
|
|
||||||
*ngIf="
|
|
||||||
selectableProduct.product != productTypes.Free &&
|
|
||||||
selectableProduct.product != productTypes.TeamsStarter &&
|
|
||||||
selectableProduct.PasswordManager.maxSeats
|
|
||||||
"
|
|
||||||
>•
|
|
||||||
{{ "addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="!selectableProduct.PasswordManager.maxSeats"
|
|
||||||
>• {{ "addShareUnlimitedUsers" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.PasswordManager.maxCollections"
|
|
||||||
>•
|
|
||||||
{{
|
|
||||||
"limitedCollections" | i18n: selectableProduct.PasswordManager.maxCollections
|
|
||||||
}}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.PasswordManager.maxAdditionalSeats"
|
|
||||||
>•
|
|
||||||
{{
|
|
||||||
"addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxAdditionalSeats
|
|
||||||
}}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="!selectableProduct.PasswordManager.maxCollections"
|
|
||||||
>• {{ "createUnlimitedCollections" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.PasswordManager.baseStorageGb"
|
|
||||||
>•
|
|
||||||
{{
|
|
||||||
"gbEncryptedFileStorage"
|
|
||||||
| i18n: selectableProduct.PasswordManager.baseStorageGb + "GB"
|
|
||||||
}}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.hasGroups"
|
|
||||||
>• {{ "controlAccessWithGroups" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.hasApi">• {{ "trackAuditLogs" | i18n }}</small>
|
|
||||||
<small *ngIf="selectableProduct.hasDirectory"
|
|
||||||
>• {{ "syncUsersFromDirectory" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.hasSelfHost"
|
|
||||||
>• {{ "onPremHostingOptional" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.usersGetPremium">• {{ "usersGetPremium" | i18n }}</small>
|
|
||||||
<small *ngIf="selectableProduct.product != productTypes.Free"
|
|
||||||
>• {{ "priorityCustomerSupport" | i18n }}</small
|
|
||||||
>
|
|
||||||
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization"
|
|
||||||
>•
|
|
||||||
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
|
||||||
</small>
|
|
||||||
</ng-template>
|
|
||||||
</ng-template>
|
|
||||||
<span *ngIf="selectableProduct.product != productTypes.Free">
|
|
||||||
<ng-container *ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship">
|
|
||||||
{{
|
|
||||||
(selectableProduct.isAnnual
|
|
||||||
? selectableProduct.PasswordManager.basePrice / 12
|
|
||||||
: selectableProduct.PasswordManager.basePrice
|
|
||||||
) | currency: "$"
|
|
||||||
}}
|
|
||||||
/{{ "month" | i18n }},
|
|
||||||
{{ "includesXUsers" | i18n: selectableProduct.PasswordManager.baseSeats }}
|
|
||||||
<ng-container *ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption">
|
|
||||||
{{ ("additionalUsers" | i18n).toLowerCase() }}
|
|
||||||
{{
|
{{
|
||||||
(selectableProduct.isAnnual
|
(selectableProduct.isAnnual
|
||||||
? selectableProduct.PasswordManager.seatPrice / 12
|
? selectableProduct.PasswordManager.basePrice / 12
|
||||||
: selectableProduct.PasswordManager.seatPrice
|
: selectableProduct.PasswordManager.basePrice
|
||||||
) | currency: "$"
|
) | currency: "$"
|
||||||
}}
|
}}
|
||||||
/{{ "month" | i18n }}
|
/{{ "month" | i18n }},
|
||||||
</ng-container>
|
{{ "includesXUsers" | i18n: selectableProduct.PasswordManager.baseSeats }}
|
||||||
</ng-container>
|
<ng-container *ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption">
|
||||||
</span>
|
{{ ("additionalUsers" | i18n).toLowerCase() }}
|
||||||
<span
|
{{
|
||||||
*ngIf="
|
(selectableProduct.isAnnual
|
||||||
!selectableProduct.PasswordManager.basePrice &&
|
|
||||||
selectableProduct.PasswordManager.hasAdditionalSeatsOption
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
"costPerUser"
|
|
||||||
| i18n
|
|
||||||
: ((selectableProduct.isAnnual
|
|
||||||
? selectableProduct.PasswordManager.seatPrice / 12
|
? selectableProduct.PasswordManager.seatPrice / 12
|
||||||
: selectableProduct.PasswordManager.seatPrice
|
: selectableProduct.PasswordManager.seatPrice
|
||||||
)
|
) | currency: "$"
|
||||||
| currency: "$")
|
}}
|
||||||
}}
|
/{{ "month" | i18n }}
|
||||||
/{{ "month" | i18n }}
|
</ng-container>
|
||||||
</span>
|
</ng-container>
|
||||||
<span *ngIf="selectableProduct.product == productTypes.Free">{{ "freeForever" | i18n }}</span>
|
</span>
|
||||||
</label>
|
<span
|
||||||
</div>
|
*ngIf="
|
||||||
<div *ngIf="formGroup.value.product !== productTypes.Free">
|
!selectableProduct.PasswordManager.basePrice &&
|
||||||
<ng-container
|
selectableProduct.PasswordManager.hasAdditionalSeatsOption
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
"costPerUser"
|
||||||
|
| i18n
|
||||||
|
: ((selectableProduct.isAnnual
|
||||||
|
? selectableProduct.PasswordManager.seatPrice / 12
|
||||||
|
: selectableProduct.PasswordManager.seatPrice
|
||||||
|
)
|
||||||
|
| currency: "$")
|
||||||
|
}}
|
||||||
|
/{{ "month" | i18n }}
|
||||||
|
</span>
|
||||||
|
<span *ngIf="selectableProduct.product == productTypes.Free">{{
|
||||||
|
"freeForever" | i18n
|
||||||
|
}}</span>
|
||||||
|
</bit-radio-group>
|
||||||
|
</div>
|
||||||
|
</bit-section>
|
||||||
|
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
|
||||||
|
<bit-section
|
||||||
*ngIf="
|
*ngIf="
|
||||||
selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
|
selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
|
||||||
!selectedPlan.PasswordManager.baseSeats
|
!selectedPlan.PasswordManager.baseSeats
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<h2 class="mt-5">{{ "users" | i18n }}</h2>
|
<h2 bitTypography="h2">{{ "users" | i18n }}</h2>
|
||||||
<div class="row">
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
<div class="col-6">
|
<bit-form-field class="tw-col-span-6">
|
||||||
<label for="additionalSeats">{{ "userSeats" | i18n }}</label>
|
<bit-label>{{ "userSeats" | i18n }}</bit-label>
|
||||||
<input
|
<input
|
||||||
id="additionalSeats"
|
bitInput
|
||||||
class="form-control"
|
|
||||||
type="number"
|
type="number"
|
||||||
name="additionalSeats"
|
|
||||||
formControlName="additionalSeats"
|
formControlName="additionalSeats"
|
||||||
placeholder="{{ 'userSeatsDesc' | i18n }}"
|
placeholder="{{ 'userSeatsDesc' | i18n }}"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "userSeatsHowManyDesc" | i18n }}</small>
|
<bit-hint class="tw-text-sm">{{ "userSeatsHowManyDesc" | i18n }}</bit-hint>
|
||||||
</div>
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</bit-section>
|
||||||
<h2 class="mt-5">{{ "addons" | i18n }}</h2>
|
<bit-section>
|
||||||
<div
|
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||||
class="row"
|
<div
|
||||||
*ngIf="
|
class="tw-grid tw-grid-cols-12 tw-gap-4"
|
||||||
selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
|
*ngIf="
|
||||||
selectedPlan.PasswordManager.baseSeats
|
selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
|
||||||
"
|
selectedPlan.PasswordManager.baseSeats
|
||||||
>
|
"
|
||||||
<div class="form-group col-6">
|
>
|
||||||
<label for="additionalSeats">{{ "additionalUserSeats" | i18n }}</label>
|
<bit-form-field class="tw-col-span-6">
|
||||||
<input
|
<bit-label>{{ "additionalUserSeats" | i18n }}</bit-label>
|
||||||
id="additionalSeats"
|
|
||||||
class="form-control"
|
|
||||||
type="number"
|
|
||||||
name="additionalSeats"
|
|
||||||
formControlName="additionalSeats"
|
|
||||||
placeholder="{{ 'userSeatsDesc' | i18n }}"
|
|
||||||
/>
|
|
||||||
<small class="text-muted form-text">{{
|
|
||||||
"userSeatsAdditionalDesc"
|
|
||||||
| i18n
|
|
||||||
: selectedPlan.PasswordManager.baseSeats
|
|
||||||
: (seatPriceMonthly(selectedPlan) | currency: "$")
|
|
||||||
}}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="form-group col-6">
|
|
||||||
<label for="additionalStorage">{{ "additionalStorageGb" | i18n }}</label>
|
|
||||||
<input
|
|
||||||
id="additionalStorage"
|
|
||||||
class="form-control"
|
|
||||||
type="number"
|
|
||||||
name="additionalStorageGb"
|
|
||||||
formControlName="additionalStorage"
|
|
||||||
step="1"
|
|
||||||
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
|
||||||
/>
|
|
||||||
<small class="text-muted form-text">{{
|
|
||||||
"additionalStorageIntervalDesc"
|
|
||||||
| i18n
|
|
||||||
: "1 GB"
|
|
||||||
: (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
|
|
||||||
: ("month" | i18n)
|
|
||||||
}}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="form-group col-6" *ngIf="selectedPlan.PasswordManager.hasPremiumAccessOption">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
<input
|
||||||
id="premiumAccess"
|
bitInput
|
||||||
class="form-check-input"
|
type="number"
|
||||||
type="checkbox"
|
formControlName="additionalSeats"
|
||||||
name="premiumAccessAddon"
|
placeholder="{{ 'userSeatsDesc' | i18n }}"
|
||||||
formControlName="premiumAccessAddon"
|
|
||||||
/>
|
/>
|
||||||
<label for="premiumAccess" class="form-check-label bold">{{
|
<bit-hint class="tx-text-sm"
|
||||||
"premiumAccess" | i18n
|
>{{
|
||||||
}}</label>
|
"userSeatsAdditionalDesc"
|
||||||
</div>
|
| i18n
|
||||||
<small class="text-muted form-text">{{
|
: selectedPlan.PasswordManager.baseSeats
|
||||||
"premiumAccessDesc" | i18n: (3.33 | currency: "$") : ("month" | i18n)
|
: (seatPriceMonthly(selectedPlan) | currency: "$")
|
||||||
}}</small>
|
}}
|
||||||
|
</bit-hint>
|
||||||
|
</bit-form-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||||
<h2 class="spaced-header">{{ "summary" | i18n }}</h2>
|
<bit-form-field class="tw-col-span-6">
|
||||||
<div class="form-check form-check-block" *ngFor="let selectablePlan of selectablePlans">
|
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
bitInput
|
||||||
type="radio"
|
type="number"
|
||||||
name="plan"
|
formControlName="additionalStorage"
|
||||||
id="interval{{ selectablePlan.type }}"
|
step="1"
|
||||||
[value]="selectablePlan.type"
|
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||||
formControlName="plan"
|
/>
|
||||||
/>
|
<bit-hint class="tw-text-sm">{{
|
||||||
<label class="form-check-label" for="interval{{ selectablePlan.type }}">
|
"additionalStorageIntervalDesc"
|
||||||
<ng-container *ngIf="selectablePlan.isAnnual">
|
| i18n
|
||||||
{{ "annually" | i18n }}
|
: "1 GB"
|
||||||
<small *ngIf="selectablePlan.PasswordManager.basePrice">
|
: (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
|
||||||
{{ "basePrice" | i18n }}:
|
: ("month" | i18n)
|
||||||
{{
|
}}</bit-hint>
|
||||||
(selectablePlan.isAnnual
|
</bit-form-field>
|
||||||
? selectablePlan.PasswordManager.basePrice / 12
|
</div>
|
||||||
: selectablePlan.PasswordManager.basePrice
|
</bit-section>
|
||||||
) | currency: "$"
|
<bit-section>
|
||||||
}}
|
<div
|
||||||
× 12
|
class="tw-grid tw-grid-cols-12 tw-gap-4"
|
||||||
{{ "monthAbbr" | i18n }}
|
*ngIf="selectedPlan.PasswordManager.hasPremiumAccessOption"
|
||||||
=
|
>
|
||||||
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
<bit-form-control class="tw-col-span-6">
|
||||||
<span style="text-decoration: line-through">{{
|
<bit-label>{{ "premiumAccess" | i18n }}</bit-label>
|
||||||
selectablePlan.PasswordManager.basePrice | currency: "$"
|
<input type="checkbox" bitCheckbox formControlName="premiumAccessAddon" />
|
||||||
}}</span>
|
<bit-hint class="tw-text-sm">{{
|
||||||
{{ "freeWithSponsorship" | i18n }}
|
"premiumAccessDesc" | i18n: (3.33 | currency: "$") : ("month" | i18n)
|
||||||
</ng-container>
|
}}</bit-hint>
|
||||||
<ng-template #notAcceptingSponsorship>
|
</bit-form-control>
|
||||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
</div>
|
||||||
|
</bit-section>
|
||||||
|
<bit-section *ngFor="let selectablePlan of selectablePlans">
|
||||||
|
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||||
|
<bit-radio-group formControlName="plan">
|
||||||
|
<bit-radio-button
|
||||||
|
type="radio"
|
||||||
|
id="interval{{ selectablePlan.type }}"
|
||||||
|
[value]="selectablePlan.type"
|
||||||
|
>
|
||||||
|
<bit-label>{{ (selectablePlan.isAnnual ? "annually" : "monthly") | i18n }}</bit-label>
|
||||||
|
<bit-hint *ngIf="selectablePlan.isAnnual">
|
||||||
|
<p
|
||||||
|
class="tw-mb-0"
|
||||||
|
bitTypography="body2"
|
||||||
|
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||||
|
>
|
||||||
|
{{ "basePrice" | i18n }}:
|
||||||
|
{{
|
||||||
|
(selectablePlan.isAnnual
|
||||||
|
? selectablePlan.PasswordManager.basePrice / 12
|
||||||
|
: selectablePlan.PasswordManager.basePrice
|
||||||
|
) | currency: "$"
|
||||||
|
}}
|
||||||
|
× 12
|
||||||
|
{{ "monthAbbr" | i18n }}
|
||||||
|
=
|
||||||
|
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
||||||
|
<span class="tw-line-through">{{
|
||||||
|
selectablePlan.PasswordManager.basePrice | currency: "$"
|
||||||
|
}}</span>
|
||||||
|
{{ "freeWithSponsorship" | i18n }}
|
||||||
|
</ng-container>
|
||||||
|
<ng-template #notAcceptingSponsorship>
|
||||||
|
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||||
|
/{{ "year" | i18n }}
|
||||||
|
</ng-template>
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="tw-mb-0"
|
||||||
|
bitTypography="body2"
|
||||||
|
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||||
|
>
|
||||||
|
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||||
|
>{{ "additionalUsers" | i18n }}:</span
|
||||||
|
>
|
||||||
|
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||||
|
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||||
|
{{
|
||||||
|
(selectablePlan.isAnnual
|
||||||
|
? selectablePlan.PasswordManager.seatPrice / 12
|
||||||
|
: selectablePlan.PasswordManager.seatPrice
|
||||||
|
) | currency: "$"
|
||||||
|
}}
|
||||||
|
× 12 {{ "monthAbbr" | i18n }} =
|
||||||
|
{{
|
||||||
|
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||||
|
| currency: "$"
|
||||||
|
}}
|
||||||
/{{ "year" | i18n }}
|
/{{ "year" | i18n }}
|
||||||
</ng-template>
|
</p>
|
||||||
</small>
|
<p
|
||||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption">
|
class="tw-mb-0"
|
||||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
bitTypography="body2"
|
||||||
>{{ "additionalUsers" | i18n }}:</span
|
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||||
>
|
>
|
||||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
{{ "additionalStorageGb" | i18n }}:
|
||||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||||
{{
|
{{
|
||||||
(selectablePlan.isAnnual
|
(selectablePlan.isAnnual
|
||||||
? selectablePlan.PasswordManager.seatPrice / 12
|
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
|
||||||
: selectablePlan.PasswordManager.seatPrice
|
: selectablePlan.PasswordManager.additionalStoragePricePerGb
|
||||||
) | currency: "$"
|
) | currency: "$"
|
||||||
}}
|
}}
|
||||||
× 12 {{ "monthAbbr" | i18n }} =
|
× 12 {{ "monthAbbr" | i18n }} =
|
||||||
{{
|
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
|
||||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
</p>
|
||||||
| currency: "$"
|
</bit-hint>
|
||||||
}}
|
<bit-hint *ngIf="!selectablePlan.isAnnual">
|
||||||
/{{ "year" | i18n }}
|
<p
|
||||||
</small>
|
class="tw-mb-0"
|
||||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption">
|
bitTypography="body2"
|
||||||
{{ "additionalStorageGb" | i18n }}:
|
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
|
||||||
{{
|
|
||||||
(selectablePlan.isAnnual
|
|
||||||
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
|
|
||||||
: selectablePlan.PasswordManager.additionalStoragePricePerGb
|
|
||||||
) | currency: "$"
|
|
||||||
}}
|
|
||||||
× 12 {{ "monthAbbr" | i18n }} =
|
|
||||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
|
|
||||||
</small>
|
|
||||||
</ng-container>
|
|
||||||
<ng-container *ngIf="!selectablePlan.isAnnual">
|
|
||||||
{{ "monthly" | i18n }}
|
|
||||||
<small *ngIf="selectablePlan.PasswordManager.basePrice">
|
|
||||||
{{ "basePrice" | i18n }}:
|
|
||||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
|
||||||
{{ "monthAbbr" | i18n }}
|
|
||||||
=
|
|
||||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
|
||||||
/{{ "month" | i18n }}
|
|
||||||
</small>
|
|
||||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption">
|
|
||||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
|
||||||
>{{ "additionalUsers" | i18n }}:</span
|
|
||||||
>
|
>
|
||||||
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
{{ "basePrice" | i18n }}:
|
||||||
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||||
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
|
{{ "monthAbbr" | i18n }}
|
||||||
{{ "monthAbbr" | i18n }} =
|
=
|
||||||
{{
|
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||||
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
/{{ "month" | i18n }}
|
||||||
| currency: "$"
|
</p>
|
||||||
}}
|
<p
|
||||||
/{{ "month" | i18n }}
|
class="tw-mb-0"
|
||||||
</small>
|
bitTypography="body2"
|
||||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption">
|
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||||
{{ "additionalStorageGb" | i18n }}:
|
>
|
||||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||||
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
|
>{{ "additionalUsers" | i18n }}:</span
|
||||||
{{ "monthAbbr" | i18n }} =
|
>
|
||||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
|
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
|
||||||
</small>
|
{{ formGroup.controls["additionalSeats"].value || 0 }} ×
|
||||||
</ng-container>
|
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
|
||||||
</label>
|
{{ "monthAbbr" | i18n }} =
|
||||||
</div>
|
{{
|
||||||
</div>
|
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
|
||||||
|
| currency: "$"
|
||||||
|
}}
|
||||||
|
/{{ "month" | i18n }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
class="tw-mb-0"
|
||||||
|
bitTypography="body2"
|
||||||
|
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||||
|
>
|
||||||
|
{{ "additionalStorageGb" | i18n }}:
|
||||||
|
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||||
|
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
|
||||||
|
{{ "monthAbbr" | i18n }} =
|
||||||
|
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
|
||||||
|
</p>
|
||||||
|
</bit-hint>
|
||||||
|
</bit-radio-button>
|
||||||
|
</bit-radio-group>
|
||||||
|
</bit-section>
|
||||||
|
</bit-section>
|
||||||
|
|
||||||
<!-- Secrets Manager -->
|
<!-- Secrets Manager -->
|
||||||
<div class="tw-my-10">
|
<bit-section>
|
||||||
<sm-subscribe
|
<sm-subscribe
|
||||||
*ngIf="planOffersSecretsManager && !hasProvider"
|
*ngIf="planOffersSecretsManager && !hasProvider"
|
||||||
[formGroup]="formGroup.controls.secretsManager"
|
[formGroup]="formGroup.controls.secretsManager"
|
||||||
[selectedPlan]="selectedSecretsManagerPlan"
|
[selectedPlan]="selectedSecretsManagerPlan"
|
||||||
[upgradeOrganization]="!createOrganization"
|
[upgradeOrganization]="!createOrganization"
|
||||||
></sm-subscribe>
|
></sm-subscribe>
|
||||||
</div>
|
</bit-section>
|
||||||
|
|
||||||
<!-- Payment info -->
|
<!-- Payment info -->
|
||||||
<div *ngIf="formGroup.value.product !== productTypes.Free">
|
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
|
||||||
<h2 class="mb-4">
|
<h2 bitTypography="h2">
|
||||||
{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}
|
{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}
|
||||||
</h2>
|
</h2>
|
||||||
<small class="text-muted font-italic mb-3 d-block">
|
<p class="tw-text-muted tw-italic tw-mb-3 tw-block" bitTypography="body2">
|
||||||
{{ paymentDesc }}
|
{{ paymentDesc }}
|
||||||
</small>
|
</p>
|
||||||
<app-payment
|
<app-payment
|
||||||
*ngIf="createOrganization || upgradeRequiresPaymentMethod"
|
*ngIf="createOrganization || upgradeRequiresPaymentMethod"
|
||||||
[hideCredit]="true"
|
[hideCredit]="true"
|
||||||
></app-payment>
|
></app-payment>
|
||||||
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
|
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
|
||||||
<div id="price" class="my-4">
|
<div id="price" class="tw-my-4">
|
||||||
<div class="text-muted text-sm">
|
<div class="tw-text-muted tw-text-base">
|
||||||
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }}
|
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }}
|
||||||
<br />
|
<br />
|
||||||
<span *ngIf="planOffersSecretsManager && formGroup.value.secretsManager.enabled">
|
<span *ngIf="planOffersSecretsManager && formGroup.value.secretsManager.enabled">
|
||||||
|
@ -405,8 +433,8 @@
|
||||||
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
|
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
<hr class="my-1 col-3 ml-0" />
|
<hr class="tw-my-1 tw-grid tw-grid-cols-3 tw-ml-0" />
|
||||||
<p class="text-lg">
|
<p class="tw-text-lg">
|
||||||
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{
|
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{
|
||||||
selectedPlanInterval | i18n
|
selectedPlanInterval | i18n
|
||||||
}}
|
}}
|
||||||
|
@ -415,22 +443,29 @@
|
||||||
<ng-container *ngIf="!createOrganization">
|
<ng-container *ngIf="!createOrganization">
|
||||||
<app-payment [showMethods]="false"></app-payment>
|
<app-payment [showMethods]="false"></app-payment>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</bit-section>
|
||||||
<div *ngIf="singleOrgPolicyBlock" class="mt-4">
|
<bit-section *ngIf="singleOrgPolicyBlock">
|
||||||
<app-callout [type]="'error'">{{ "singleOrgBlockCreateMessage" | i18n }}</app-callout>
|
<app-callout [type]="'error'">{{ "singleOrgBlockCreateMessage" | i18n }}</app-callout>
|
||||||
</div>
|
</bit-section>
|
||||||
<div class="mt-4">
|
<bit-section>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
buttonType="primary"
|
buttonType="primary"
|
||||||
bitButton
|
bitButton
|
||||||
[loading]="form.loading"
|
bitFormButton
|
||||||
[disabled]="!formGroup.valid"
|
[disabled]="!formGroup.valid"
|
||||||
>
|
>
|
||||||
{{ "submit" | i18n }}
|
{{ "submit" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button type="button" buttonType="secondary" bitButton (click)="cancel()" *ngIf="showCancel">
|
<button
|
||||||
|
type="button"
|
||||||
|
buttonType="secondary"
|
||||||
|
bitButton
|
||||||
|
bitFormButton
|
||||||
|
(click)="cancel()"
|
||||||
|
*ngIf="showCancel"
|
||||||
|
>
|
||||||
{{ "cancel" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</bit-section>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -70,6 +70,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
@Input() showCancel = false;
|
@Input() showCancel = false;
|
||||||
@Input() acceptingSponsorship = false;
|
@Input() acceptingSponsorship = false;
|
||||||
@Input() currentPlan: PlanResponse;
|
@Input() currentPlan: PlanResponse;
|
||||||
|
selectedFile: File;
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
get product(): ProductType {
|
get product(): ProductType {
|
||||||
|
@ -109,6 +110,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
|
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
|
||||||
|
|
||||||
|
selfHostedForm = this.formBuilder.group({
|
||||||
|
file: [null, [Validators.required]],
|
||||||
|
});
|
||||||
|
|
||||||
formGroup = this.formBuilder.group({
|
formGroup = this.formBuilder.group({
|
||||||
name: [""],
|
name: [""],
|
||||||
billingEmail: ["", [Validators.email]],
|
billingEmail: ["", [Validators.email]],
|
||||||
|
@ -527,72 +532,71 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
this.onCanceled.emit();
|
this.onCanceled.emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
async submit() {
|
setSelectedFile(event: Event) {
|
||||||
|
const fileInputEl = <HTMLInputElement>event.target;
|
||||||
|
this.selectedFile = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
if (this.singleOrgPolicyBlock) {
|
if (this.singleOrgPolicyBlock) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const doSubmit = async (): Promise<string> => {
|
||||||
|
let orgId: string = null;
|
||||||
|
if (this.createOrganization) {
|
||||||
|
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
|
||||||
|
const key = orgKey[0].encryptedString;
|
||||||
|
const collection = await this.cryptoService.encrypt(
|
||||||
|
this.i18nService.t("defaultCollection"),
|
||||||
|
orgKey[1],
|
||||||
|
);
|
||||||
|
const collectionCt = collection.encryptedString;
|
||||||
|
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
|
||||||
|
|
||||||
try {
|
if (this.selfHosted) {
|
||||||
const doSubmit = async (): Promise<string> => {
|
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
|
||||||
let orgId: string = null;
|
|
||||||
if (this.createOrganization) {
|
|
||||||
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
|
|
||||||
const key = orgKey[0].encryptedString;
|
|
||||||
const collection = await this.cryptoService.encrypt(
|
|
||||||
this.i18nService.t("defaultCollection"),
|
|
||||||
orgKey[1],
|
|
||||||
);
|
|
||||||
const collectionCt = collection.encryptedString;
|
|
||||||
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
|
|
||||||
|
|
||||||
if (this.selfHosted) {
|
|
||||||
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
|
|
||||||
} else {
|
|
||||||
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
this.i18nService.t("organizationCreated"),
|
|
||||||
this.i18nService.t("organizationReadyToGo"),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
orgId = await this.updateOrganization(orgId);
|
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
|
||||||
this.platformUtilsService.showToast(
|
|
||||||
"success",
|
|
||||||
null,
|
|
||||||
this.i18nService.t("organizationUpgraded"),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.apiService.refreshIdentityToken();
|
this.platformUtilsService.showToast(
|
||||||
await this.syncService.fullSync(true);
|
"success",
|
||||||
|
this.i18nService.t("organizationCreated"),
|
||||||
|
this.i18nService.t("organizationReadyToGo"),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
orgId = await this.updateOrganization(orgId);
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("organizationUpgraded"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
|
await this.apiService.refreshIdentityToken();
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.syncService.fullSync(true);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.router.navigate(["/organizations/" + orgId]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isInTrialFlow) {
|
if (!this.acceptingSponsorship && !this.isInTrialFlow) {
|
||||||
this.onTrialBillingSuccess.emit({
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
orgId: orgId,
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
subLabelText: this.billingSubLabelText(),
|
this.router.navigate(["/organizations/" + orgId]);
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return orgId;
|
if (this.isInTrialFlow) {
|
||||||
};
|
this.onTrialBillingSuccess.emit({
|
||||||
|
orgId: orgId,
|
||||||
|
subLabelText: this.billingSubLabelText(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.formPromise = doSubmit();
|
return orgId;
|
||||||
const organizationId = await this.formPromise;
|
};
|
||||||
this.onSuccess.emit({ organizationId: organizationId });
|
|
||||||
// TODO: No one actually listening to this message?
|
this.formPromise = doSubmit();
|
||||||
this.messagingService.send("organizationCreated", { organizationId });
|
const organizationId = await this.formPromise;
|
||||||
} catch (e) {
|
this.onSuccess.emit({ organizationId: organizationId });
|
||||||
this.logService.error(e);
|
this.messagingService.send("organizationCreated", organizationId);
|
||||||
}
|
};
|
||||||
}
|
|
||||||
|
|
||||||
private async updateOrganization(orgId: string) {
|
private async updateOrganization(orgId: string) {
|
||||||
const request = new OrganizationUpgradeRequest();
|
const request = new OrganizationUpgradeRequest();
|
||||||
|
@ -693,14 +697,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
|
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
|
||||||
const fileEl = document.getElementById("file") as HTMLInputElement;
|
if (!this.selectedFile) {
|
||||||
const files = fileEl.files;
|
|
||||||
if (files == null || files.length === 0) {
|
|
||||||
throw new Error(this.i18nService.t("selectFile"));
|
throw new Error(this.i18nService.t("selectFile"));
|
||||||
}
|
}
|
||||||
|
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("license", files[0]);
|
fd.append("license", this.selectedFile);
|
||||||
fd.append("key", key);
|
fd.append("key", key);
|
||||||
fd.append("collectionName", collectionCt);
|
fd.append("collectionName", collectionCt);
|
||||||
const response = await this.organizationApiService.createLicense(fd);
|
const response = await this.organizationApiService.createLicense(fd);
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
{{ "addOrganization" | i18n }}
|
{{ "addOrganization" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
|
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
|
||||||
{{ "close" | i18n }}
|
{{ "cancel" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</bit-dialog>
|
</bit-dialog>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Subject, takeUntil } from "rxjs";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
@ -45,6 +46,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
protected dialogService: DialogService,
|
protected dialogService: DialogService,
|
||||||
protected kdfConfigService: KdfConfigService,
|
protected kdfConfigService: KdfConfigService,
|
||||||
|
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Router } from "@angular/router";
|
||||||
import { firstValueFrom, Subject } from "rxjs";
|
import { firstValueFrom, Subject } from "rxjs";
|
||||||
import { concatMap, map, take, takeUntil } from "rxjs/operators";
|
import { concatMap, map, take, takeUntil } from "rxjs/operators";
|
||||||
|
|
||||||
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common";
|
import { PinServiceAbstraction, PinLockType } from "@bitwarden/auth/common";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||||
|
@ -30,7 +30,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||||
import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums";
|
||||||
import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { UserId } from "@bitwarden/common/types/guid";
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
|
@ -55,7 +54,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||||
protected onSuccessfulSubmit: () => Promise<void>;
|
protected onSuccessfulSubmit: () => Promise<void>;
|
||||||
|
|
||||||
private invalidPinAttempts = 0;
|
private invalidPinAttempts = 0;
|
||||||
private pinStatus: PinLockType;
|
private pinLockType: PinLockType;
|
||||||
|
|
||||||
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
|
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
|
||||||
|
|
||||||
|
@ -81,7 +80,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||||
protected dialogService: DialogService,
|
protected dialogService: DialogService,
|
||||||
protected deviceTrustService: DeviceTrustServiceAbstraction,
|
protected deviceTrustService: DeviceTrustServiceAbstraction,
|
||||||
protected userVerificationService: UserVerificationService,
|
protected userVerificationService: UserVerificationService,
|
||||||
protected pinCryptoService: PinCryptoServiceAbstraction,
|
protected pinService: PinServiceAbstraction,
|
||||||
protected biometricStateService: BiometricStateService,
|
protected biometricStateService: BiometricStateService,
|
||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
protected authService: AuthService,
|
protected authService: AuthService,
|
||||||
|
@ -168,7 +167,8 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||||
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
|
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userKey = await this.pinCryptoService.decryptUserKeyWithPin(this.pin);
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
|
const userKey = await this.pinService.decryptUserKeyWithPin(this.pin, userId);
|
||||||
|
|
||||||
if (userKey) {
|
if (userKey) {
|
||||||
await this.setUserKeyAndContinue(userKey);
|
await this.setUserKeyAndContinue(userKey);
|
||||||
|
@ -272,7 +272,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||||
await this.setUserKeyAndContinue(userKey, true);
|
await this.setUserKeyAndContinue(userKey, true);
|
||||||
}
|
}
|
||||||
|
@ -358,12 +358,13 @@ export class LockComponent implements OnInit, OnDestroy {
|
||||||
return await this.vaultTimeoutService.logOut(userId);
|
return await this.vaultTimeoutService.logOut(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
|
this.pinLockType = await this.pinService.getPinLockType(userId);
|
||||||
|
|
||||||
|
const ephemeralPinSet = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId);
|
||||||
|
|
||||||
let ephemeralPinSet = await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
|
|
||||||
ephemeralPinSet ||= await this.stateService.getDecryptedPinProtected();
|
|
||||||
this.pinEnabled =
|
this.pinEnabled =
|
||||||
(this.pinStatus === "TRANSIENT" && !!ephemeralPinSet) || this.pinStatus === "PERSISTANT";
|
(this.pinLockType === "EPHEMERAL" && !!ephemeralPinSet) || this.pinLockType === "PERSISTENT";
|
||||||
|
|
||||||
this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword();
|
this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword();
|
||||||
|
|
||||||
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
|
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
|
||||||
|
|
|
@ -52,7 +52,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
messagingService: MessagingService,
|
messagingService: MessagingService,
|
||||||
|
@ -82,6 +82,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
||||||
stateService,
|
stateService,
|
||||||
dialogService,
|
dialogService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
|
masterPasswordService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,63 +1,66 @@
|
||||||
import { DialogRef } from "@angular/cdk/dialog";
|
import { DialogRef } from "@angular/cdk/dialog";
|
||||||
import { Directive, OnInit } from "@angular/core";
|
import { Directive, OnInit } from "@angular/core";
|
||||||
import { FormBuilder, Validators } from "@angular/forms";
|
import { FormBuilder, Validators } from "@angular/forms";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class SetPinComponent implements OnInit {
|
export class SetPinComponent implements OnInit {
|
||||||
showMasterPassOnRestart = true;
|
showMasterPasswordOnClientRestartOption = true;
|
||||||
|
|
||||||
setPinForm = this.formBuilder.group({
|
setPinForm = this.formBuilder.group({
|
||||||
pin: ["", [Validators.required]],
|
pin: ["", [Validators.required]],
|
||||||
masterPassOnRestart: true,
|
requireMasterPasswordOnClientRestart: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private dialogRef: DialogRef,
|
private accountService: AccountService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
private userVerificationService: UserVerificationService,
|
private dialogRef: DialogRef,
|
||||||
private stateService: StateService,
|
|
||||||
private formBuilder: FormBuilder,
|
private formBuilder: FormBuilder,
|
||||||
private kdfConfigService: KdfConfigService,
|
private pinService: PinServiceAbstraction,
|
||||||
|
private userVerificationService: UserVerificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
||||||
|
|
||||||
this.setPinForm.controls.masterPassOnRestart.setValue(hasMasterPassword);
|
this.setPinForm.controls.requireMasterPasswordOnClientRestart.setValue(hasMasterPassword);
|
||||||
this.showMasterPassOnRestart = hasMasterPassword;
|
this.showMasterPasswordOnClientRestartOption = hasMasterPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
const pin = this.setPinForm.get("pin").value;
|
const pin = this.setPinForm.get("pin").value;
|
||||||
const masterPassOnRestart = this.setPinForm.get("masterPassOnRestart").value;
|
const requireMasterPasswordOnClientRestart = this.setPinForm.get(
|
||||||
|
"requireMasterPasswordOnClientRestart",
|
||||||
|
).value;
|
||||||
|
|
||||||
if (Utils.isNullOrWhitespace(pin)) {
|
if (Utils.isNullOrWhitespace(pin)) {
|
||||||
this.dialogRef.close(false);
|
this.dialogRef.close(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinKey = await this.cryptoService.makePinKey(
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
pin,
|
|
||||||
await this.stateService.getEmail(),
|
|
||||||
await this.kdfConfigService.getKdfConfig(),
|
|
||||||
);
|
|
||||||
const userKey = await this.cryptoService.getUserKey();
|
const userKey = await this.cryptoService.getUserKey();
|
||||||
const pinProtectedKey = await this.cryptoService.encrypt(userKey.key, pinKey);
|
|
||||||
const encPin = await this.cryptoService.encrypt(pin, userKey);
|
|
||||||
|
|
||||||
await this.stateService.setProtectedPin(encPin.encryptedString);
|
const userKeyEncryptedPin = await this.pinService.createUserKeyEncryptedPin(pin, userKey);
|
||||||
|
await this.pinService.setUserKeyEncryptedPin(userKeyEncryptedPin, userId);
|
||||||
|
|
||||||
if (masterPassOnRestart) {
|
const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
|
||||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(pinProtectedKey);
|
pin,
|
||||||
} else {
|
userKey,
|
||||||
await this.stateService.setPinKeyEncryptedUserKey(pinProtectedKey);
|
userId,
|
||||||
}
|
);
|
||||||
|
await this.pinService.storePinKeyEncryptedUserKey(
|
||||||
|
pinKeyEncryptedUserKey,
|
||||||
|
requireMasterPasswordOnClientRestart,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
this.dialogRef.close(true);
|
this.dialogRef.close(true);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
|
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||||
|
@ -46,6 +47,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
kdfConfigService: KdfConfigService,
|
kdfConfigService: KdfConfigService,
|
||||||
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
i18nService,
|
i18nService,
|
||||||
|
@ -57,6 +59,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
||||||
stateService,
|
stateService,
|
||||||
dialogService,
|
dialogService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
|
masterPasswordService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
|
||||||
dialogService: DialogService,
|
dialogService: DialogService,
|
||||||
kdfConfigService: KdfConfigService,
|
kdfConfigService: KdfConfigService,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
i18nService,
|
i18nService,
|
||||||
|
@ -74,6 +74,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
|
||||||
stateService,
|
stateService,
|
||||||
dialogService,
|
dialogService,
|
||||||
kdfConfigService,
|
kdfConfigService,
|
||||||
|
masterPasswordService,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { Subject } from "rxjs";
|
||||||
import {
|
import {
|
||||||
AuthRequestServiceAbstraction,
|
AuthRequestServiceAbstraction,
|
||||||
AuthRequestService,
|
AuthRequestService,
|
||||||
PinCryptoServiceAbstraction,
|
PinServiceAbstraction,
|
||||||
PinCryptoService,
|
PinService,
|
||||||
LoginStrategyServiceAbstraction,
|
LoginStrategyServiceAbstraction,
|
||||||
LoginStrategyService,
|
LoginStrategyService,
|
||||||
LoginEmailServiceAbstraction,
|
LoginEmailServiceAbstraction,
|
||||||
|
@ -545,6 +545,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
provide: CryptoServiceAbstraction,
|
provide: CryptoServiceAbstraction,
|
||||||
useClass: CryptoService,
|
useClass: CryptoService,
|
||||||
deps: [
|
deps: [
|
||||||
|
PinServiceAbstraction,
|
||||||
InternalMasterPasswordServiceAbstraction,
|
InternalMasterPasswordServiceAbstraction,
|
||||||
KeyGenerationServiceAbstraction,
|
KeyGenerationServiceAbstraction,
|
||||||
CryptoFunctionServiceAbstraction,
|
CryptoFunctionServiceAbstraction,
|
||||||
|
@ -661,6 +662,8 @@ const safeProviders: SafeProvider[] = [
|
||||||
provide: VaultTimeoutSettingsServiceAbstraction,
|
provide: VaultTimeoutSettingsServiceAbstraction,
|
||||||
useClass: VaultTimeoutSettingsService,
|
useClass: VaultTimeoutSettingsService,
|
||||||
deps: [
|
deps: [
|
||||||
|
AccountServiceAbstraction,
|
||||||
|
PinServiceAbstraction,
|
||||||
UserDecryptionOptionsServiceAbstraction,
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
TokenServiceAbstraction,
|
TokenServiceAbstraction,
|
||||||
|
@ -728,6 +731,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
CollectionServiceAbstraction,
|
CollectionServiceAbstraction,
|
||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
|
PinServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
@ -736,6 +740,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
deps: [
|
deps: [
|
||||||
FolderServiceAbstraction,
|
FolderServiceAbstraction,
|
||||||
CipherServiceAbstraction,
|
CipherServiceAbstraction,
|
||||||
|
PinServiceAbstraction,
|
||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
CryptoFunctionServiceAbstraction,
|
CryptoFunctionServiceAbstraction,
|
||||||
KdfConfigServiceAbstraction,
|
KdfConfigServiceAbstraction,
|
||||||
|
@ -747,6 +752,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
deps: [
|
deps: [
|
||||||
CipherServiceAbstraction,
|
CipherServiceAbstraction,
|
||||||
ApiServiceAbstraction,
|
ApiServiceAbstraction,
|
||||||
|
PinServiceAbstraction,
|
||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
CryptoFunctionServiceAbstraction,
|
CryptoFunctionServiceAbstraction,
|
||||||
CollectionServiceAbstraction,
|
CollectionServiceAbstraction,
|
||||||
|
@ -822,7 +828,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: InternalMasterPasswordServiceAbstraction,
|
provide: InternalMasterPasswordServiceAbstraction,
|
||||||
useClass: MasterPasswordService,
|
useClass: MasterPasswordService,
|
||||||
deps: [StateProvider],
|
deps: [StateProvider, StateServiceAbstraction, KeyGenerationServiceAbstraction, EncryptService],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: MasterPasswordServiceAbstraction,
|
provide: MasterPasswordServiceAbstraction,
|
||||||
|
@ -855,7 +861,7 @@ const safeProviders: SafeProvider[] = [
|
||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
UserVerificationApiServiceAbstraction,
|
UserVerificationApiServiceAbstraction,
|
||||||
UserDecryptionOptionsServiceAbstraction,
|
UserDecryptionOptionsServiceAbstraction,
|
||||||
PinCryptoServiceAbstraction,
|
PinServiceAbstraction,
|
||||||
LogService,
|
LogService,
|
||||||
VaultTimeoutSettingsServiceAbstraction,
|
VaultTimeoutSettingsServiceAbstraction,
|
||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
|
@ -1005,14 +1011,18 @@ const safeProviders: SafeProvider[] = [
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
provide: PinCryptoServiceAbstraction,
|
provide: PinServiceAbstraction,
|
||||||
useClass: PinCryptoService,
|
useClass: PinService,
|
||||||
deps: [
|
deps: [
|
||||||
StateServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
CryptoServiceAbstraction,
|
CryptoFunctionServiceAbstraction,
|
||||||
VaultTimeoutSettingsServiceAbstraction,
|
EncryptService,
|
||||||
LogService,
|
|
||||||
KdfConfigServiceAbstraction,
|
KdfConfigServiceAbstraction,
|
||||||
|
KeyGenerationServiceAbstraction,
|
||||||
|
LogService,
|
||||||
|
MasterPasswordServiceAbstraction,
|
||||||
|
StateProvider,
|
||||||
|
StateServiceAbstraction,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
</h1>
|
</h1>
|
||||||
<p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p>
|
<p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="tw-mb-auto tw-mx-auto tw-grid">
|
<div class="tw-mb-auto tw-mx-auto tw-flex tw-flex-col tw-items-center">
|
||||||
<div
|
<div
|
||||||
class="tw-rounded-xl tw-mb-9 tw-mx-auto sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"
|
class="tw-rounded-xl tw-mb-9 tw-mx-auto sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export * from "./pin-crypto.service.abstraction";
|
export * from "./pin.service.abstraction";
|
||||||
export * from "./login-email.service";
|
export * from "./login-email.service";
|
||||||
export * from "./login-strategy.service";
|
export * from "./login-strategy.service";
|
||||||
export * from "./user-decryption-options.service.abstraction";
|
export * from "./user-decryption-options.service.abstraction";
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
|
||||||
|
|
||||||
export abstract class PinCryptoServiceAbstraction {
|
|
||||||
decryptUserKeyWithPin: (pin: string) => Promise<UserKey | null>;
|
|
||||||
}
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||||
|
import { EncString, EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { PinKey, UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
|
import { PinLockType } from "../services";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The PinService is used for PIN-based unlocks. Below is a very basic overview of the PIN flow:
|
||||||
|
*
|
||||||
|
* -- Setting the PIN via {@link SetPinComponent} --
|
||||||
|
*
|
||||||
|
* When the user submits the setPinForm:
|
||||||
|
|
||||||
|
* 1. We encrypt the PIN with the UserKey and store it on disk as `userKeyEncryptedPin`.
|
||||||
|
*
|
||||||
|
* 2. We create a PinKey from the PIN, and then use that PinKey to encrypt the UserKey, resulting in
|
||||||
|
* a `pinKeyEncryptedUserKey`, which can be stored in one of two ways depending on what the user selects
|
||||||
|
* for the `requireMasterPasswordOnClientReset` checkbox.
|
||||||
|
*
|
||||||
|
* If `requireMasterPasswordOnClientReset` is:
|
||||||
|
* - TRUE, store in memory as `pinKeyEncryptedUserKeyEphemeral` (does NOT persist through a client reset)
|
||||||
|
* - FALSE, store on disk as `pinKeyEncryptedUserKeyPersistent` (persists through a client reset)
|
||||||
|
*
|
||||||
|
* -- Unlocking with the PIN via {@link LockComponent} --
|
||||||
|
*
|
||||||
|
* When the user enters their PIN, we decrypt their UserKey with the PIN and set that UserKey to state.
|
||||||
|
*/
|
||||||
|
export abstract class PinServiceAbstraction {
|
||||||
|
/**
|
||||||
|
* Gets the persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
|
||||||
|
*/
|
||||||
|
abstract getPinKeyEncryptedUserKeyPersistent: (userId: UserId) => Promise<EncString>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
|
||||||
|
*/
|
||||||
|
abstract clearPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
|
||||||
|
*/
|
||||||
|
abstract getPinKeyEncryptedUserKeyEphemeral: (userId: UserId) => Promise<EncString>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
|
||||||
|
*/
|
||||||
|
abstract clearPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a pinKeyEncryptedUserKey from the provided PIN and UserKey.
|
||||||
|
*/
|
||||||
|
abstract createPinKeyEncryptedUserKey: (
|
||||||
|
pin: string,
|
||||||
|
userKey: UserKey,
|
||||||
|
userId: UserId,
|
||||||
|
) => Promise<EncString>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores the UserKey, encrypted by the PinKey.
|
||||||
|
* @param storeEphemeralVersion If true, stores an ephemeral version via the private {@link setPinKeyEncryptedUserKeyEphemeral} method.
|
||||||
|
* If false, stores a persistent version via the private {@link setPinKeyEncryptedUserKeyPersistent} method.
|
||||||
|
*/
|
||||||
|
abstract storePinKeyEncryptedUserKey: (
|
||||||
|
pinKeyEncryptedUserKey: EncString,
|
||||||
|
storeEphemeralVersion: boolean,
|
||||||
|
userId: UserId,
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the user's PIN, encrypted by the UserKey.
|
||||||
|
*/
|
||||||
|
abstract getUserKeyEncryptedPin: (userId: UserId) => Promise<EncString>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the user's PIN, encrypted by the UserKey.
|
||||||
|
*/
|
||||||
|
abstract setUserKeyEncryptedPin: (
|
||||||
|
userKeyEncryptedPin: EncString,
|
||||||
|
userId: UserId,
|
||||||
|
) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a PIN, encrypted by the UserKey.
|
||||||
|
*/
|
||||||
|
abstract createUserKeyEncryptedPin: (pin: string, userKey: UserKey) => Promise<EncString>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the user's PIN, encrypted by the UserKey.
|
||||||
|
*/
|
||||||
|
abstract clearUserKeyEncryptedPin(userId: UserId): Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the old MasterKey, encrypted by the PinKey (formerly called `pinProtected`).
|
||||||
|
* Deprecated and used for migration purposes only.
|
||||||
|
*/
|
||||||
|
abstract getOldPinKeyEncryptedMasterKey: (userId: UserId) => Promise<EncryptedString>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the old MasterKey, encrypted by the PinKey.
|
||||||
|
*/
|
||||||
|
abstract clearOldPinKeyEncryptedMasterKey: (userId: UserId) => Promise<void>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes a PinKey from the provided PIN.
|
||||||
|
*/
|
||||||
|
abstract makePinKey: (pin: string, salt: string, kdfConfig: KdfConfig) => Promise<PinKey>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the user's PinLockType {@link PinLockType}.
|
||||||
|
*/
|
||||||
|
abstract getPinLockType: (userId: UserId) => Promise<PinLockType>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declares whether or not the user has a PIN set (either persistent or ephemeral).
|
||||||
|
*/
|
||||||
|
abstract isPinSet: (userId: UserId) => Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the UserKey with the provided PIN.
|
||||||
|
*
|
||||||
|
* @remarks - If the user has an old pinKeyEncryptedMasterKey (formerly called `pinProtected`), the UserKey
|
||||||
|
* will be obtained via the private {@link decryptAndMigrateOldPinKeyEncryptedMasterKey} method.
|
||||||
|
* - If the user does not have an old pinKeyEncryptedMasterKey, the UserKey will be obtained via the
|
||||||
|
* private {@link decryptUserKey} method.
|
||||||
|
* @returns UserKey
|
||||||
|
*/
|
||||||
|
abstract decryptUserKeyWithPin: (pin: string, userId: UserId) => Promise<UserKey | null>;
|
||||||
|
}
|
|
@ -127,7 +127,7 @@ describe("AuthRequestLoginStrategy", () => {
|
||||||
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||||
|
|
||||||
masterPasswordService.masterKeySubject.next(masterKey);
|
masterPasswordService.masterKeySubject.next(masterKey);
|
||||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId });
|
tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId });
|
||||||
|
|
||||||
await authRequestLoginStrategy.logIn(credentials);
|
await authRequestLoginStrategy.logIn(credentials);
|
||||||
|
|
|
@ -156,7 +156,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||||
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
|
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (masterKey) {
|
if (masterKey) {
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
await this.cryptoService.setUserKey(userKey);
|
await this.cryptoService.setUserKey(userKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -262,7 +262,7 @@ describe("LoginStrategy", () => {
|
||||||
|
|
||||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
masterPasswordService.masterKeySubject.next(masterKey);
|
masterPasswordService.masterKeySubject.next(masterKey);
|
||||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||||
|
|
||||||
const result = await passwordLoginStrategy.logIn(credentials);
|
const result = await passwordLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
|
@ -281,7 +281,7 @@ describe("LoginStrategy", () => {
|
||||||
|
|
||||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
masterPasswordService.masterKeySubject.next(masterKey);
|
masterPasswordService.masterKeySubject.next(masterKey);
|
||||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||||
|
|
||||||
await passwordLoginStrategy.logIn(credentials);
|
await passwordLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
|
|
|
@ -163,7 +163,7 @@ describe("PasswordLoginStrategy", () => {
|
||||||
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||||
|
|
||||||
masterPasswordService.masterKeySubject.next(masterKey);
|
masterPasswordService.masterKeySubject.next(masterKey);
|
||||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
|
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
|
||||||
|
|
||||||
await passwordLoginStrategy.logIn(credentials);
|
await passwordLoginStrategy.logIn(credentials);
|
||||||
|
|
|
@ -228,7 +228,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
||||||
|
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (masterKey) {
|
if (masterKey) {
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
await this.cryptoService.setUserKey(userKey, userId);
|
await this.cryptoService.setUserKey(userKey, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -445,11 +445,15 @@ describe("SsoLoginStrategy", () => {
|
||||||
|
|
||||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
masterPasswordService.masterKeySubject.next(masterKey);
|
masterPasswordService.masterKeySubject.next(masterKey);
|
||||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||||
|
|
||||||
await ssoLoginStrategy.logIn(credentials);
|
await ssoLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||||
|
masterKey,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -497,11 +501,15 @@ describe("SsoLoginStrategy", () => {
|
||||||
|
|
||||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
masterPasswordService.masterKeySubject.next(masterKey);
|
masterPasswordService.masterKeySubject.next(masterKey);
|
||||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||||
|
|
||||||
await ssoLoginStrategy.logIn(credentials);
|
await ssoLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||||
|
masterKey,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -350,7 +350,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
await this.cryptoService.setUserKey(userKey);
|
await this.cryptoService.setUserKey(userKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -190,11 +190,15 @@ describe("UserApiLoginStrategy", () => {
|
||||||
|
|
||||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
masterPasswordService.masterKeySubject.next(masterKey);
|
masterPasswordService.masterKeySubject.next(masterKey);
|
||||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||||
|
|
||||||
await apiLogInStrategy.logIn(credentials);
|
await apiLogInStrategy.logIn(credentials);
|
||||||
|
|
||||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||||
|
masterKey,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -110,7 +110,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
||||||
if (response.apiUseKeyConnector) {
|
if (response.apiUseKeyConnector) {
|
||||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (masterKey) {
|
if (masterKey) {
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
await this.cryptoService.setUserKey(userKey, userId);
|
await this.cryptoService.setUserKey(userKey, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,7 +172,9 @@ describe("AuthRequestService", () => {
|
||||||
|
|
||||||
masterPasswordService.masterKeySubject.next(undefined);
|
masterPasswordService.masterKeySubject.next(undefined);
|
||||||
masterPasswordService.masterKeyHashSubject.next(undefined);
|
masterPasswordService.masterKeyHashSubject.next(undefined);
|
||||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey);
|
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(
|
||||||
|
mockDecryptedUserKey,
|
||||||
|
);
|
||||||
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
|
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
@ -192,8 +194,10 @@ describe("AuthRequestService", () => {
|
||||||
mockDecryptedMasterKeyHash,
|
mockDecryptedMasterKeyHash,
|
||||||
mockUserId,
|
mockUserId,
|
||||||
);
|
);
|
||||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||||
mockDecryptedMasterKey,
|
mockDecryptedMasterKey,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey);
|
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey);
|
||||||
});
|
});
|
||||||
|
|
|
@ -178,7 +178,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Decrypt and set user key in state
|
// Decrypt and set user key in state
|
||||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||||
|
|
||||||
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
|
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export * from "./pin-crypto/pin-crypto.service.implementation";
|
export * from "./pin/pin.service.implementation";
|
||||||
export * from "./login-email/login-email.service";
|
export * from "./login-email/login-email.service";
|
||||||
export * from "./login-strategies/login-strategy.service";
|
export * from "./login-strategies/login-strategy.service";
|
||||||
export * from "./user-decryption-options/user-decryption-options.service";
|
export * from "./user-decryption-options/user-decryption-options.service";
|
||||||
|
|
|
@ -1,104 +0,0 @@
|
||||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
|
||||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
|
||||||
import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
|
||||||
|
|
||||||
import { PinCryptoServiceAbstraction } from "../../abstractions/pin-crypto.service.abstraction";
|
|
||||||
|
|
||||||
export class PinCryptoService implements PinCryptoServiceAbstraction {
|
|
||||||
constructor(
|
|
||||||
private stateService: StateService,
|
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsService,
|
|
||||||
private logService: LogService,
|
|
||||||
private kdfConfigService: KdfConfigService,
|
|
||||||
) {}
|
|
||||||
async decryptUserKeyWithPin(pin: string): Promise<UserKey | null> {
|
|
||||||
try {
|
|
||||||
const pinLockType: PinLockType = await this.vaultTimeoutSettingsService.isPinLockSet();
|
|
||||||
|
|
||||||
const { pinKeyEncryptedUserKey, oldPinKeyEncryptedMasterKey } =
|
|
||||||
await this.getPinKeyEncryptedKeys(pinLockType);
|
|
||||||
|
|
||||||
const kdfConfig: KdfConfig = await this.kdfConfigService.getKdfConfig();
|
|
||||||
let userKey: UserKey;
|
|
||||||
const email = await this.stateService.getEmail();
|
|
||||||
if (oldPinKeyEncryptedMasterKey) {
|
|
||||||
userKey = await this.cryptoService.decryptAndMigrateOldPinKey(
|
|
||||||
pinLockType === "TRANSIENT",
|
|
||||||
pin,
|
|
||||||
email,
|
|
||||||
kdfConfig,
|
|
||||||
oldPinKeyEncryptedMasterKey,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
userKey = await this.cryptoService.decryptUserKeyWithPin(
|
|
||||||
pin,
|
|
||||||
email,
|
|
||||||
kdfConfig,
|
|
||||||
pinKeyEncryptedUserKey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userKey) {
|
|
||||||
this.logService.warning(`User key null after pin key decryption.`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(await this.validatePin(userKey, pin))) {
|
|
||||||
this.logService.warning(`Pin key decryption successful but pin validation failed.`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return userKey;
|
|
||||||
} catch (error) {
|
|
||||||
this.logService.error(`Error decrypting user key with pin: ${error}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: oldPinKeyEncryptedMasterKey is only used for migrating old pin keys
|
|
||||||
// and will be null for all migrated accounts
|
|
||||||
private async getPinKeyEncryptedKeys(
|
|
||||||
pinLockType: PinLockType,
|
|
||||||
): Promise<{ pinKeyEncryptedUserKey: EncString; oldPinKeyEncryptedMasterKey?: EncString }> {
|
|
||||||
switch (pinLockType) {
|
|
||||||
case "PERSISTANT": {
|
|
||||||
const pinKeyEncryptedUserKey = await this.stateService.getPinKeyEncryptedUserKey();
|
|
||||||
const oldPinKeyEncryptedMasterKey = await this.stateService.getEncryptedPinProtected();
|
|
||||||
return {
|
|
||||||
pinKeyEncryptedUserKey,
|
|
||||||
oldPinKeyEncryptedMasterKey: oldPinKeyEncryptedMasterKey
|
|
||||||
? new EncString(oldPinKeyEncryptedMasterKey)
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case "TRANSIENT": {
|
|
||||||
const pinKeyEncryptedUserKey = await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
|
|
||||||
const oldPinKeyEncryptedMasterKey = await this.stateService.getDecryptedPinProtected();
|
|
||||||
return { pinKeyEncryptedUserKey, oldPinKeyEncryptedMasterKey };
|
|
||||||
}
|
|
||||||
case "DISABLED":
|
|
||||||
throw new Error("Pin is disabled");
|
|
||||||
default: {
|
|
||||||
// Compile-time check for exhaustive switch
|
|
||||||
const _exhaustiveCheck: never = pinLockType;
|
|
||||||
return _exhaustiveCheck;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async validatePin(userKey: UserKey, pin: string): Promise<boolean> {
|
|
||||||
const protectedPin = await this.stateService.getProtectedPin();
|
|
||||||
const decryptedPin = await this.cryptoService.decryptToUtf8(
|
|
||||||
new EncString(protectedPin),
|
|
||||||
userKey,
|
|
||||||
);
|
|
||||||
return decryptedPin === pin;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,192 +0,0 @@
|
||||||
import { mock } from "jest-mock-extended";
|
|
||||||
|
|
||||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums";
|
|
||||||
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
|
||||||
import {
|
|
||||||
VaultTimeoutSettingsService,
|
|
||||||
PinLockType,
|
|
||||||
} from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
|
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
|
||||||
|
|
||||||
import { PinCryptoService } from "./pin-crypto.service.implementation";
|
|
||||||
|
|
||||||
describe("PinCryptoService", () => {
|
|
||||||
let pinCryptoService: PinCryptoService;
|
|
||||||
|
|
||||||
const stateService = mock<StateService>();
|
|
||||||
const cryptoService = mock<CryptoService>();
|
|
||||||
const vaultTimeoutSettingsService = mock<VaultTimeoutSettingsService>();
|
|
||||||
const logService = mock<LogService>();
|
|
||||||
const kdfConfigService = mock<KdfConfigService>();
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
pinCryptoService = new PinCryptoService(
|
|
||||||
stateService,
|
|
||||||
cryptoService,
|
|
||||||
vaultTimeoutSettingsService,
|
|
||||||
logService,
|
|
||||||
kdfConfigService,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("instantiates", () => {
|
|
||||||
expect(pinCryptoService).not.toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("decryptUserKeyWithPin(...)", () => {
|
|
||||||
const mockPin = "1234";
|
|
||||||
const mockProtectedPin = "protectedPin";
|
|
||||||
const mockUserEmail = "user@example.com";
|
|
||||||
const mockUserKey = new SymmetricCryptoKey(randomBytes(32)) as UserKey;
|
|
||||||
|
|
||||||
function setupDecryptUserKeyWithPinMocks(
|
|
||||||
pinLockType: PinLockType,
|
|
||||||
migrationStatus: "PRE" | "POST" = "POST",
|
|
||||||
) {
|
|
||||||
vaultTimeoutSettingsService.isPinLockSet.mockResolvedValue(pinLockType);
|
|
||||||
|
|
||||||
kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG);
|
|
||||||
stateService.getEmail.mockResolvedValue(mockUserEmail);
|
|
||||||
|
|
||||||
if (migrationStatus === "PRE") {
|
|
||||||
cryptoService.decryptAndMigrateOldPinKey.mockResolvedValue(mockUserKey);
|
|
||||||
} else {
|
|
||||||
cryptoService.decryptUserKeyWithPin.mockResolvedValue(mockUserKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
mockPinEncryptedKeyDataByPinLockType(pinLockType, migrationStatus);
|
|
||||||
|
|
||||||
stateService.getProtectedPin.mockResolvedValue(mockProtectedPin);
|
|
||||||
cryptoService.decryptToUtf8.mockResolvedValue(mockPin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: both pinKeyEncryptedUserKeys use encryptionType: 2 (AesCbc256_HmacSha256_B64)
|
|
||||||
const pinKeyEncryptedUserKeyEphemeral = new EncString(
|
|
||||||
"2.gbauOANURUHqvhLTDnva1A==|nSW+fPumiuTaDB/s12+JO88uemV6rhwRSR+YR1ZzGr5j6Ei3/h+XEli2Unpz652NlZ9NTuRpHxeOqkYYJtp7J+lPMoclgteXuAzUu9kqlRc=|DeUFkhIwgkGdZA08bDnDqMMNmZk21D+H5g8IostPKAY=",
|
|
||||||
);
|
|
||||||
|
|
||||||
const pinKeyEncryptedUserKeyPersistant = new EncString(
|
|
||||||
"2.fb5kOEZvh9zPABbP8WRmSQ==|Yi6ZAJY+UtqCKMUSqp1ahY9Kf8QuneKXs6BMkpNsakLVOzTYkHHlilyGABMF7GzUO8QHyZi7V/Ovjjg+Naf3Sm8qNhxtDhibITv4k8rDnM0=|TFkq3h2VNTT1z5BFbebm37WYuxyEHXuRo0DZJI7TQnw=",
|
|
||||||
);
|
|
||||||
|
|
||||||
const oldPinKeyEncryptedMasterKeyPostMigration: any = null;
|
|
||||||
const oldPinKeyEncryptedMasterKeyPreMigrationPersistent =
|
|
||||||
"2.fb5kOEZvh9zPABbP8WRmSQ==|Yi6ZAJY+UtqCKMUSqp1ahY9Kf8QuneKXs6BMkpNsakLVOzTYkHHlilyGABMF7GzUO8QHyZi7V/Ovjjg+Naf3Sm8qNhxtDhibITv4k8rDnM0=|TFkq3h2VNTT1z5BFbebm37WYuxyEHXuRo0DZJI7TQnw=";
|
|
||||||
const oldPinKeyEncryptedMasterKeyPreMigrationEphemeral = new EncString(
|
|
||||||
"2.fb5kOEZvh9zPABbP8WRmSQ==|Yi6ZAJY+UtqCKMUSqp1ahY9Kf8QuneKXs6BMkpNsakLVOzTYkHHlilyGABMF7GzUO8QHyZi7V/Ovjjg+Naf3Sm8qNhxtDhibITv4k8rDnM0=|TFkq3h2VNTT1z5BFbebm37WYuxyEHXuRo0DZJI7TQnw=",
|
|
||||||
);
|
|
||||||
|
|
||||||
function mockPinEncryptedKeyDataByPinLockType(
|
|
||||||
pinLockType: PinLockType,
|
|
||||||
migrationStatus: "PRE" | "POST" = "POST",
|
|
||||||
) {
|
|
||||||
switch (pinLockType) {
|
|
||||||
case "PERSISTANT":
|
|
||||||
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(
|
|
||||||
pinKeyEncryptedUserKeyPersistant,
|
|
||||||
);
|
|
||||||
if (migrationStatus === "PRE") {
|
|
||||||
stateService.getEncryptedPinProtected.mockResolvedValue(
|
|
||||||
oldPinKeyEncryptedMasterKeyPreMigrationPersistent,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
stateService.getEncryptedPinProtected.mockResolvedValue(
|
|
||||||
oldPinKeyEncryptedMasterKeyPostMigration,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "TRANSIENT":
|
|
||||||
stateService.getPinKeyEncryptedUserKeyEphemeral.mockResolvedValue(
|
|
||||||
pinKeyEncryptedUserKeyEphemeral,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (migrationStatus === "PRE") {
|
|
||||||
stateService.getDecryptedPinProtected.mockResolvedValue(
|
|
||||||
oldPinKeyEncryptedMasterKeyPreMigrationEphemeral,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
stateService.getDecryptedPinProtected.mockResolvedValue(
|
|
||||||
oldPinKeyEncryptedMasterKeyPostMigration,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "DISABLED":
|
|
||||||
// no mocking required. Error should be thrown
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const testCases: { pinLockType: PinLockType; migrationStatus: "PRE" | "POST" }[] = [
|
|
||||||
{ pinLockType: "PERSISTANT", migrationStatus: "PRE" },
|
|
||||||
{ pinLockType: "PERSISTANT", migrationStatus: "POST" },
|
|
||||||
{ pinLockType: "TRANSIENT", migrationStatus: "PRE" },
|
|
||||||
{ pinLockType: "TRANSIENT", migrationStatus: "POST" },
|
|
||||||
];
|
|
||||||
|
|
||||||
testCases.forEach(({ pinLockType, migrationStatus }) => {
|
|
||||||
describe(`given a ${pinLockType} PIN (${migrationStatus} migration)`, () => {
|
|
||||||
it(`should successfully decrypt and return user key when using a valid PIN`, async () => {
|
|
||||||
// Arrange
|
|
||||||
setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = await pinCryptoService.decryptUserKeyWithPin(mockPin);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result).toEqual(mockUserKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should return null when PIN is incorrect and user key cannot be decrypted`, async () => {
|
|
||||||
// Arrange
|
|
||||||
setupDecryptUserKeyWithPinMocks("PERSISTANT");
|
|
||||||
|
|
||||||
cryptoService.decryptUserKeyWithPin.mockResolvedValue(null);
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = await pinCryptoService.decryptUserKeyWithPin(mockPin);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
// not sure if this is a realistic scenario but going to test it anyway
|
|
||||||
it(`should return null when PIN doesn't match after successful user key decryption`, async () => {
|
|
||||||
// Arrange
|
|
||||||
setupDecryptUserKeyWithPinMocks("PERSISTANT");
|
|
||||||
|
|
||||||
// non matching PIN
|
|
||||||
cryptoService.decryptToUtf8.mockResolvedValue("9999");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = await pinCryptoService.decryptUserKeyWithPin(mockPin);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it(`should return null when pin is disabled`, async () => {
|
|
||||||
// Arrange
|
|
||||||
setupDecryptUserKeyWithPinMocks("DISABLED");
|
|
||||||
|
|
||||||
// Act
|
|
||||||
const result = await pinCryptoService.decryptUserKeyWithPin(mockPin);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(result).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Test helpers
|
|
||||||
function randomBytes(length: number): Uint8Array {
|
|
||||||
return new Uint8Array(Array.from({ length }, (_, k) => k % 255));
|
|
||||||
}
|
|
|
@ -0,0 +1,501 @@
|
||||||
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
|
import { MasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||||
|
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||||
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { EncString, EncryptedString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import {
|
||||||
|
PIN_DISK,
|
||||||
|
PIN_MEMORY,
|
||||||
|
StateProvider,
|
||||||
|
UserKeyDefinition,
|
||||||
|
} from "@bitwarden/common/platform/state";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { MasterKey, PinKey, UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
|
import { PinServiceAbstraction } from "../../abstractions/pin.service.abstraction";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - DISABLED : No PIN set.
|
||||||
|
* - PERSISTENT : PIN is set and persists through client reset.
|
||||||
|
* - EPHEMERAL : PIN is set, but does NOT persist through client reset. This means that
|
||||||
|
* after client reset the master password is required to unlock.
|
||||||
|
*/
|
||||||
|
export type PinLockType = "DISABLED" | "PERSISTENT" | "EPHEMERAL";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
|
||||||
|
*
|
||||||
|
* @remarks Persists through a client reset. Used when `requireMasterPasswordOnClientRestart` is disabled.
|
||||||
|
* @see SetPinComponent.setPinForm.requireMasterPasswordOnClientRestart
|
||||||
|
*/
|
||||||
|
export const PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT = new UserKeyDefinition<EncryptedString>(
|
||||||
|
PIN_DISK,
|
||||||
|
"pinKeyEncryptedUserKeyPersistent",
|
||||||
|
{
|
||||||
|
deserializer: (jsonValue) => jsonValue,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
|
||||||
|
*
|
||||||
|
* @remarks Does NOT persist through a client reset. Used when `requireMasterPasswordOnClientRestart` is enabled.
|
||||||
|
* @see SetPinComponent.setPinForm.requireMasterPasswordOnClientRestart
|
||||||
|
*/
|
||||||
|
export const PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL = new UserKeyDefinition<EncryptedString>(
|
||||||
|
PIN_MEMORY,
|
||||||
|
"pinKeyEncryptedUserKeyEphemeral",
|
||||||
|
{
|
||||||
|
deserializer: (jsonValue) => jsonValue,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The PIN, encrypted by the UserKey.
|
||||||
|
*/
|
||||||
|
export const USER_KEY_ENCRYPTED_PIN = new UserKeyDefinition<EncryptedString>(
|
||||||
|
PIN_DISK,
|
||||||
|
"userKeyEncryptedPin",
|
||||||
|
{
|
||||||
|
deserializer: (jsonValue) => jsonValue,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The old MasterKey, encrypted by the PinKey (formerly called `pinProtected`).
|
||||||
|
* Deprecated and used for migration purposes only.
|
||||||
|
*/
|
||||||
|
export const OLD_PIN_KEY_ENCRYPTED_MASTER_KEY = new UserKeyDefinition<EncryptedString>(
|
||||||
|
PIN_DISK,
|
||||||
|
"oldPinKeyEncryptedMasterKey",
|
||||||
|
{
|
||||||
|
deserializer: (jsonValue) => jsonValue,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export class PinService implements PinServiceAbstraction {
|
||||||
|
constructor(
|
||||||
|
private accountService: AccountService,
|
||||||
|
private cryptoFunctionService: CryptoFunctionService,
|
||||||
|
private encryptService: EncryptService,
|
||||||
|
private kdfConfigService: KdfConfigService,
|
||||||
|
private keyGenerationService: KeyGenerationService,
|
||||||
|
private logService: LogService,
|
||||||
|
private masterPasswordService: MasterPasswordServiceAbstraction,
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
private stateService: StateService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async getPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<EncString> {
|
||||||
|
this.validateUserId(userId, "Cannot get pinKeyEncryptedUserKeyPersistent.");
|
||||||
|
|
||||||
|
return EncString.fromJSON(
|
||||||
|
await firstValueFrom(
|
||||||
|
this.stateProvider.getUserState$(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the persistent (stored on disk) version of the UserKey, encrypted by the PinKey.
|
||||||
|
*/
|
||||||
|
private async setPinKeyEncryptedUserKeyPersistent(
|
||||||
|
pinKeyEncryptedUserKey: EncString,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<void> {
|
||||||
|
this.validateUserId(userId, "Cannot set pinKeyEncryptedUserKeyPersistent.");
|
||||||
|
|
||||||
|
if (pinKeyEncryptedUserKey == null) {
|
||||||
|
throw new Error(
|
||||||
|
"No pinKeyEncryptedUserKey provided. Cannot set pinKeyEncryptedUserKeyPersistent.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(
|
||||||
|
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||||
|
pinKeyEncryptedUserKey?.encryptedString,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearPinKeyEncryptedUserKeyPersistent(userId: UserId): Promise<void> {
|
||||||
|
this.validateUserId(userId, "Cannot clear pinKeyEncryptedUserKeyPersistent.");
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT, null, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise<EncString> {
|
||||||
|
this.validateUserId(userId, "Cannot get pinKeyEncryptedUserKeyEphemeral.");
|
||||||
|
|
||||||
|
return EncString.fromJSON(
|
||||||
|
await firstValueFrom(
|
||||||
|
this.stateProvider.getUserState$(PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL, userId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the ephemeral (stored in memory) version of the UserKey, encrypted by the PinKey.
|
||||||
|
*/
|
||||||
|
private async setPinKeyEncryptedUserKeyEphemeral(
|
||||||
|
pinKeyEncryptedUserKey: EncString,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<void> {
|
||||||
|
this.validateUserId(userId, "Cannot set pinKeyEncryptedUserKeyEphemeral.");
|
||||||
|
|
||||||
|
if (pinKeyEncryptedUserKey == null) {
|
||||||
|
throw new Error(
|
||||||
|
"No pinKeyEncryptedUserKey provided. Cannot set pinKeyEncryptedUserKeyEphemeral.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(
|
||||||
|
PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL,
|
||||||
|
pinKeyEncryptedUserKey?.encryptedString,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearPinKeyEncryptedUserKeyEphemeral(userId: UserId): Promise<void> {
|
||||||
|
this.validateUserId(userId, "Cannot clear pinKeyEncryptedUserKeyEphemeral.");
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL, null, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPinKeyEncryptedUserKey(
|
||||||
|
pin: string,
|
||||||
|
userKey: UserKey,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<EncString> {
|
||||||
|
this.validateUserId(userId, "Cannot create pinKeyEncryptedUserKey.");
|
||||||
|
|
||||||
|
if (!userKey) {
|
||||||
|
throw new Error("No UserKey provided. Cannot create pinKeyEncryptedUserKey.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = await firstValueFrom(
|
||||||
|
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)),
|
||||||
|
);
|
||||||
|
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||||
|
|
||||||
|
const pinKey = await this.makePinKey(pin, email, kdfConfig);
|
||||||
|
|
||||||
|
return await this.encryptService.encrypt(userKey.key, pinKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async storePinKeyEncryptedUserKey(
|
||||||
|
pinKeyEncryptedUserKey: EncString,
|
||||||
|
storeAsEphemeral: boolean,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<void> {
|
||||||
|
this.validateUserId(userId, "Cannot store pinKeyEncryptedUserKey.");
|
||||||
|
|
||||||
|
if (storeAsEphemeral) {
|
||||||
|
await this.setPinKeyEncryptedUserKeyEphemeral(pinKeyEncryptedUserKey, userId);
|
||||||
|
} else {
|
||||||
|
await this.setPinKeyEncryptedUserKeyPersistent(pinKeyEncryptedUserKey, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getUserKeyEncryptedPin(userId: UserId): Promise<EncString> {
|
||||||
|
this.validateUserId(userId, "Cannot get userKeyEncryptedPin.");
|
||||||
|
|
||||||
|
return EncString.fromJSON(
|
||||||
|
await firstValueFrom(this.stateProvider.getUserState$(USER_KEY_ENCRYPTED_PIN, userId)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setUserKeyEncryptedPin(userKeyEncryptedPin: EncString, userId: UserId): Promise<void> {
|
||||||
|
this.validateUserId(userId, "Cannot set userKeyEncryptedPin.");
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(
|
||||||
|
USER_KEY_ENCRYPTED_PIN,
|
||||||
|
userKeyEncryptedPin?.encryptedString,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearUserKeyEncryptedPin(userId: UserId): Promise<void> {
|
||||||
|
this.validateUserId(userId, "Cannot clear userKeyEncryptedPin.");
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(USER_KEY_ENCRYPTED_PIN, null, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createUserKeyEncryptedPin(pin: string, userKey: UserKey): Promise<EncString> {
|
||||||
|
if (!userKey) {
|
||||||
|
throw new Error("No UserKey provided. Cannot create userKeyEncryptedPin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.encryptService.encrypt(pin, userKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOldPinKeyEncryptedMasterKey(userId: UserId): Promise<EncryptedString> {
|
||||||
|
this.validateUserId(userId, "Cannot get oldPinKeyEncryptedMasterKey.");
|
||||||
|
|
||||||
|
return await firstValueFrom(
|
||||||
|
this.stateProvider.getUserState$(OLD_PIN_KEY_ENCRYPTED_MASTER_KEY, userId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearOldPinKeyEncryptedMasterKey(userId: UserId): Promise<void> {
|
||||||
|
this.validateUserId(userId, "Cannot clear oldPinKeyEncryptedMasterKey.");
|
||||||
|
|
||||||
|
await this.stateProvider.setUserState(OLD_PIN_KEY_ENCRYPTED_MASTER_KEY, null, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
|
||||||
|
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig);
|
||||||
|
return (await this.keyGenerationService.stretchKey(pinKey)) as PinKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getPinLockType(userId: UserId): Promise<PinLockType> {
|
||||||
|
this.validateUserId(userId, "Cannot get PinLockType.");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We can't check the `userKeyEncryptedPin` (formerly called `protectedPin`) for both because old
|
||||||
|
* accounts only used it for MP on Restart
|
||||||
|
*/
|
||||||
|
const aUserKeyEncryptedPinIsSet = !!(await this.getUserKeyEncryptedPin(userId));
|
||||||
|
const aPinKeyEncryptedUserKeyPersistentIsSet =
|
||||||
|
!!(await this.getPinKeyEncryptedUserKeyPersistent(userId));
|
||||||
|
const anOldPinKeyEncryptedMasterKeyIsSet =
|
||||||
|
!!(await this.getOldPinKeyEncryptedMasterKey(userId));
|
||||||
|
|
||||||
|
if (aPinKeyEncryptedUserKeyPersistentIsSet || anOldPinKeyEncryptedMasterKeyIsSet) {
|
||||||
|
return "PERSISTENT";
|
||||||
|
} else if (
|
||||||
|
aUserKeyEncryptedPinIsSet &&
|
||||||
|
!aPinKeyEncryptedUserKeyPersistentIsSet &&
|
||||||
|
!anOldPinKeyEncryptedMasterKeyIsSet
|
||||||
|
) {
|
||||||
|
return "EPHEMERAL";
|
||||||
|
} else {
|
||||||
|
return "DISABLED";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isPinSet(userId: UserId): Promise<boolean> {
|
||||||
|
this.validateUserId(userId, "Cannot determine if PIN is set.");
|
||||||
|
|
||||||
|
return (await this.getPinLockType(userId)) !== "DISABLED";
|
||||||
|
}
|
||||||
|
|
||||||
|
async decryptUserKeyWithPin(pin: string, userId: UserId): Promise<UserKey | null> {
|
||||||
|
this.validateUserId(userId, "Cannot decrypt user key with PIN.");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pinLockType = await this.getPinLockType(userId);
|
||||||
|
const requireMasterPasswordOnClientRestart = pinLockType === "EPHEMERAL";
|
||||||
|
|
||||||
|
const { pinKeyEncryptedUserKey, oldPinKeyEncryptedMasterKey } =
|
||||||
|
await this.getPinKeyEncryptedKeys(pinLockType, userId);
|
||||||
|
|
||||||
|
const email = await firstValueFrom(
|
||||||
|
this.accountService.accounts$.pipe(map((accounts) => accounts[userId].email)),
|
||||||
|
);
|
||||||
|
const kdfConfig = await this.kdfConfigService.getKdfConfig();
|
||||||
|
|
||||||
|
let userKey: UserKey;
|
||||||
|
|
||||||
|
if (oldPinKeyEncryptedMasterKey) {
|
||||||
|
userKey = await this.decryptAndMigrateOldPinKeyEncryptedMasterKey(
|
||||||
|
userId,
|
||||||
|
pin,
|
||||||
|
email,
|
||||||
|
kdfConfig,
|
||||||
|
requireMasterPasswordOnClientRestart,
|
||||||
|
oldPinKeyEncryptedMasterKey,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
userKey = await this.decryptUserKey(userId, pin, email, kdfConfig, pinKeyEncryptedUserKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userKey) {
|
||||||
|
this.logService.warning(`User key null after pin key decryption.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await this.validatePin(userKey, pin, userId))) {
|
||||||
|
this.logService.warning(`Pin key decryption successful but pin validation failed.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userKey;
|
||||||
|
} catch (error) {
|
||||||
|
this.logService.error(`Error decrypting user key with pin: ${error}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypts the UserKey with the provided PIN.
|
||||||
|
*/
|
||||||
|
private async decryptUserKey(
|
||||||
|
userId: UserId,
|
||||||
|
pin: string,
|
||||||
|
salt: string,
|
||||||
|
kdfConfig: KdfConfig,
|
||||||
|
pinKeyEncryptedUserKey?: EncString,
|
||||||
|
): Promise<UserKey> {
|
||||||
|
this.validateUserId(userId, "Cannot decrypt user key.");
|
||||||
|
|
||||||
|
pinKeyEncryptedUserKey ||= await this.getPinKeyEncryptedUserKeyPersistent(userId);
|
||||||
|
pinKeyEncryptedUserKey ||= await this.getPinKeyEncryptedUserKeyEphemeral(userId);
|
||||||
|
|
||||||
|
if (!pinKeyEncryptedUserKey) {
|
||||||
|
throw new Error("No pinKeyEncryptedUserKey found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
|
||||||
|
const userKey = await this.encryptService.decryptToBytes(pinKeyEncryptedUserKey, pinKey);
|
||||||
|
|
||||||
|
return new SymmetricCryptoKey(userKey) as UserKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new `pinKeyEncryptedUserKey` and clears the `oldPinKeyEncryptedMasterKey`.
|
||||||
|
* @returns UserKey
|
||||||
|
*/
|
||||||
|
private async decryptAndMigrateOldPinKeyEncryptedMasterKey(
|
||||||
|
userId: UserId,
|
||||||
|
pin: string,
|
||||||
|
email: string,
|
||||||
|
kdfConfig: KdfConfig,
|
||||||
|
requireMasterPasswordOnClientRestart: boolean,
|
||||||
|
oldPinKeyEncryptedMasterKey: EncString,
|
||||||
|
): Promise<UserKey> {
|
||||||
|
this.validateUserId(userId, "Cannot decrypt and migrate oldPinKeyEncryptedMasterKey.");
|
||||||
|
|
||||||
|
const masterKey = await this.decryptMasterKeyWithPin(
|
||||||
|
userId,
|
||||||
|
pin,
|
||||||
|
email,
|
||||||
|
kdfConfig,
|
||||||
|
oldPinKeyEncryptedMasterKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
const encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey({ userId: userId });
|
||||||
|
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||||
|
masterKey,
|
||||||
|
new EncString(encUserKey),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pinKeyEncryptedUserKey = await this.createPinKeyEncryptedUserKey(pin, userKey, userId);
|
||||||
|
await this.storePinKeyEncryptedUserKey(
|
||||||
|
pinKeyEncryptedUserKey,
|
||||||
|
requireMasterPasswordOnClientRestart,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const userKeyEncryptedPin = await this.createUserKeyEncryptedPin(pin, userKey);
|
||||||
|
await this.setUserKeyEncryptedPin(userKeyEncryptedPin, userId);
|
||||||
|
|
||||||
|
await this.clearOldPinKeyEncryptedMasterKey(userId);
|
||||||
|
|
||||||
|
// This also clears the old Biometrics key since the new Biometrics key will be created when the user key is set.
|
||||||
|
await this.stateService.setCryptoMasterKeyBiometric(null, { userId: userId });
|
||||||
|
|
||||||
|
return userKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only for migration purposes
|
||||||
|
private async decryptMasterKeyWithPin(
|
||||||
|
userId: UserId,
|
||||||
|
pin: string,
|
||||||
|
salt: string,
|
||||||
|
kdfConfig: KdfConfig,
|
||||||
|
oldPinKeyEncryptedMasterKey?: EncString,
|
||||||
|
): Promise<MasterKey> {
|
||||||
|
this.validateUserId(userId, "Cannot decrypt master key with PIN.");
|
||||||
|
|
||||||
|
if (!oldPinKeyEncryptedMasterKey) {
|
||||||
|
const oldPinKeyEncryptedMasterKeyString = await this.getOldPinKeyEncryptedMasterKey(userId);
|
||||||
|
|
||||||
|
if (oldPinKeyEncryptedMasterKeyString == null) {
|
||||||
|
throw new Error("No oldPinKeyEncrytedMasterKey found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
oldPinKeyEncryptedMasterKey = new EncString(oldPinKeyEncryptedMasterKeyString);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
|
||||||
|
const masterKey = await this.encryptService.decryptToBytes(oldPinKeyEncryptedMasterKey, pinKey);
|
||||||
|
|
||||||
|
return new SymmetricCryptoKey(masterKey) as MasterKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the user's `pinKeyEncryptedUserKey` (persistent or ephemeral) and `oldPinKeyEncryptedMasterKey`
|
||||||
|
* (if one exists) based on the user's PinLockType.
|
||||||
|
*
|
||||||
|
* @remarks The `oldPinKeyEncryptedMasterKey` (formerly `pinProtected`) is only used for migration and
|
||||||
|
* will be null for all migrated accounts.
|
||||||
|
* @throws If PinLockType is 'DISABLED' or if userId is not provided
|
||||||
|
*/
|
||||||
|
private async getPinKeyEncryptedKeys(
|
||||||
|
pinLockType: PinLockType,
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<{ pinKeyEncryptedUserKey: EncString; oldPinKeyEncryptedMasterKey?: EncString }> {
|
||||||
|
this.validateUserId(userId, "Cannot get PinKey encrypted keys.");
|
||||||
|
|
||||||
|
switch (pinLockType) {
|
||||||
|
case "PERSISTENT": {
|
||||||
|
const pinKeyEncryptedUserKey = await this.getPinKeyEncryptedUserKeyPersistent(userId);
|
||||||
|
const oldPinKeyEncryptedMasterKey = await this.getOldPinKeyEncryptedMasterKey(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pinKeyEncryptedUserKey,
|
||||||
|
oldPinKeyEncryptedMasterKey: oldPinKeyEncryptedMasterKey
|
||||||
|
? new EncString(oldPinKeyEncryptedMasterKey)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "EPHEMERAL": {
|
||||||
|
const pinKeyEncryptedUserKey = await this.getPinKeyEncryptedUserKeyEphemeral(userId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pinKeyEncryptedUserKey,
|
||||||
|
oldPinKeyEncryptedMasterKey: undefined, // Going forward, we only migrate non-ephemeral version
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "DISABLED":
|
||||||
|
throw new Error("Pin is disabled");
|
||||||
|
default: {
|
||||||
|
// Compile-time check for exhaustive switch
|
||||||
|
const _exhaustiveCheck: never = pinLockType;
|
||||||
|
return _exhaustiveCheck;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async validatePin(userKey: UserKey, pin: string, userId: UserId): Promise<boolean> {
|
||||||
|
this.validateUserId(userId, "Cannot validate PIN.");
|
||||||
|
|
||||||
|
const userKeyEncryptedPin = await this.getUserKeyEncryptedPin(userId);
|
||||||
|
const decryptedPin = await this.encryptService.decryptToUtf8(userKeyEncryptedPin, userKey);
|
||||||
|
|
||||||
|
const isPinValid = this.cryptoFunctionService.compareFast(decryptedPin, pin);
|
||||||
|
return isPinValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws a custom error message if user ID is not provided.
|
||||||
|
*/
|
||||||
|
private validateUserId(userId: UserId, errorMessage: string = "") {
|
||||||
|
if (!userId) {
|
||||||
|
throw new Error(`User ID is required. ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,597 @@
|
||||||
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||||
|
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
|
||||||
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { KeyGenerationService } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { DEFAULT_KDF_CONFIG } from "@bitwarden/common/platform/enums";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import {
|
||||||
|
FakeAccountService,
|
||||||
|
FakeStateProvider,
|
||||||
|
mockAccountServiceWith,
|
||||||
|
} from "@bitwarden/common/spec";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { MasterKey, PinKey, UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
|
import {
|
||||||
|
PinService,
|
||||||
|
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||||
|
PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL,
|
||||||
|
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
|
||||||
|
USER_KEY_ENCRYPTED_PIN,
|
||||||
|
PinLockType,
|
||||||
|
} from "./pin.service.implementation";
|
||||||
|
|
||||||
|
describe("PinService", () => {
|
||||||
|
let sut: PinService;
|
||||||
|
|
||||||
|
let accountService: FakeAccountService;
|
||||||
|
let masterPasswordService: FakeMasterPasswordService;
|
||||||
|
let stateProvider: FakeStateProvider;
|
||||||
|
|
||||||
|
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||||
|
const encryptService = mock<EncryptService>();
|
||||||
|
const kdfConfigService = mock<KdfConfigService>();
|
||||||
|
const keyGenerationService = mock<KeyGenerationService>();
|
||||||
|
const logService = mock<LogService>();
|
||||||
|
const stateService = mock<StateService>();
|
||||||
|
|
||||||
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
const mockUserKey = new SymmetricCryptoKey(randomBytes(64)) as UserKey;
|
||||||
|
const mockMasterKey = new SymmetricCryptoKey(randomBytes(32)) as MasterKey;
|
||||||
|
const mockPinKey = new SymmetricCryptoKey(randomBytes(32)) as PinKey;
|
||||||
|
const mockUserEmail = "user@example.com";
|
||||||
|
const mockPin = "1234";
|
||||||
|
const mockUserKeyEncryptedPin = new EncString("userKeyEncryptedPin");
|
||||||
|
|
||||||
|
// Note: both pinKeyEncryptedUserKeys use encryptionType: 2 (AesCbc256_HmacSha256_B64)
|
||||||
|
const pinKeyEncryptedUserKeyEphemeral = new EncString(
|
||||||
|
"2.gbauOANURUHqvhLTDnva1A==|nSW+fPumiuTaDB/s12+JO88uemV6rhwRSR+YR1ZzGr5j6Ei3/h+XEli2Unpz652NlZ9NTuRpHxeOqkYYJtp7J+lPMoclgteXuAzUu9kqlRc=|DeUFkhIwgkGdZA08bDnDqMMNmZk21D+H5g8IostPKAY=",
|
||||||
|
);
|
||||||
|
const pinKeyEncryptedUserKeyPersistant = new EncString(
|
||||||
|
"2.fb5kOEZvh9zPABbP8WRmSQ==|Yi6ZAJY+UtqCKMUSqp1ahY9Kf8QuneKXs6BMkpNsakLVOzTYkHHlilyGABMF7GzUO8QHyZi7V/Ovjjg+Naf3Sm8qNhxtDhibITv4k8rDnM0=|TFkq3h2VNTT1z5BFbebm37WYuxyEHXuRo0DZJI7TQnw=",
|
||||||
|
);
|
||||||
|
|
||||||
|
const oldPinKeyEncryptedMasterKeyPostMigration: any = null;
|
||||||
|
const oldPinKeyEncryptedMasterKeyPreMigrationPersistent =
|
||||||
|
"2.fb5kOEZvh9zPABbP8WRmSQ==|Yi6ZAJY+UtqCKMUSqp1ahY9Kf8QuneKXs6BMkpNsakLVOzTYkHHlilyGABMF7GzUO8QHyZi7V/Ovjjg+Naf3Sm8qNhxtDhibITv4k8rDnM0=|TFkq3h2VNTT1z5BFbebm37WYuxyEHXuRo0DZJI7TQnw=";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
accountService = mockAccountServiceWith(mockUserId, { email: mockUserEmail });
|
||||||
|
masterPasswordService = new FakeMasterPasswordService();
|
||||||
|
stateProvider = new FakeStateProvider(accountService);
|
||||||
|
|
||||||
|
sut = new PinService(
|
||||||
|
accountService,
|
||||||
|
cryptoFunctionService,
|
||||||
|
encryptService,
|
||||||
|
kdfConfigService,
|
||||||
|
keyGenerationService,
|
||||||
|
logService,
|
||||||
|
masterPasswordService,
|
||||||
|
stateProvider,
|
||||||
|
stateService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should instantiate the PinService", () => {
|
||||||
|
expect(sut).not.toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("userId validation", () => {
|
||||||
|
it("should throw an error if a userId is not provided", async () => {
|
||||||
|
await expect(sut.getPinKeyEncryptedUserKeyPersistent(undefined)).rejects.toThrow(
|
||||||
|
"User ID is required. Cannot get pinKeyEncryptedUserKeyPersistent.",
|
||||||
|
);
|
||||||
|
await expect(sut.getPinKeyEncryptedUserKeyEphemeral(undefined)).rejects.toThrow(
|
||||||
|
"User ID is required. Cannot get pinKeyEncryptedUserKeyEphemeral.",
|
||||||
|
);
|
||||||
|
await expect(sut.clearPinKeyEncryptedUserKeyPersistent(undefined)).rejects.toThrow(
|
||||||
|
"User ID is required. Cannot clear pinKeyEncryptedUserKeyPersistent.",
|
||||||
|
);
|
||||||
|
await expect(sut.clearPinKeyEncryptedUserKeyEphemeral(undefined)).rejects.toThrow(
|
||||||
|
"User ID is required. Cannot clear pinKeyEncryptedUserKeyEphemeral.",
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
sut.createPinKeyEncryptedUserKey(mockPin, mockUserKey, undefined),
|
||||||
|
).rejects.toThrow("User ID is required. Cannot create pinKeyEncryptedUserKey.");
|
||||||
|
await expect(sut.getUserKeyEncryptedPin(undefined)).rejects.toThrow(
|
||||||
|
"User ID is required. Cannot get userKeyEncryptedPin.",
|
||||||
|
);
|
||||||
|
await expect(sut.setUserKeyEncryptedPin(mockUserKeyEncryptedPin, undefined)).rejects.toThrow(
|
||||||
|
"User ID is required. Cannot set userKeyEncryptedPin.",
|
||||||
|
);
|
||||||
|
await expect(sut.clearUserKeyEncryptedPin(undefined)).rejects.toThrow(
|
||||||
|
"User ID is required. Cannot clear userKeyEncryptedPin.",
|
||||||
|
);
|
||||||
|
await expect(sut.getOldPinKeyEncryptedMasterKey(undefined)).rejects.toThrow(
|
||||||
|
"User ID is required. Cannot get oldPinKeyEncryptedMasterKey.",
|
||||||
|
);
|
||||||
|
await expect(sut.clearOldPinKeyEncryptedMasterKey(undefined)).rejects.toThrow(
|
||||||
|
"User ID is required. Cannot clear oldPinKeyEncryptedMasterKey.",
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
sut.createPinKeyEncryptedUserKey(mockPin, mockUserKey, undefined),
|
||||||
|
).rejects.toThrow("User ID is required. Cannot create pinKeyEncryptedUserKey.");
|
||||||
|
await expect(sut.getPinLockType(undefined)).rejects.toThrow("Cannot get PinLockType.");
|
||||||
|
await expect(sut.isPinSet(undefined)).rejects.toThrow(
|
||||||
|
"User ID is required. Cannot determine if PIN is set.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("get/clear/create/store pinKeyEncryptedUserKey methods", () => {
|
||||||
|
describe("getPinKeyEncryptedUserKeyPersistent()", () => {
|
||||||
|
it("should get the pinKeyEncryptedUserKey of the specified userId", async () => {
|
||||||
|
await sut.getPinKeyEncryptedUserKeyPersistent(mockUserId);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(
|
||||||
|
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("clearPinKeyEncryptedUserKeyPersistent()", () => {
|
||||||
|
it("should clear the pinKeyEncryptedUserKey of the specified userId", async () => {
|
||||||
|
await sut.clearPinKeyEncryptedUserKeyPersistent(mockUserId);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
|
||||||
|
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||||
|
null,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPinKeyEncryptedUserKeyEphemeral()", () => {
|
||||||
|
it("should get the pinKeyEncrypterUserKeyEphemeral of the specified userId", async () => {
|
||||||
|
await sut.getPinKeyEncryptedUserKeyEphemeral(mockUserId);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(
|
||||||
|
PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("clearPinKeyEncryptedUserKeyEphemeral()", () => {
|
||||||
|
it("should clear the pinKeyEncryptedUserKey of the specified userId", async () => {
|
||||||
|
await sut.clearPinKeyEncryptedUserKeyEphemeral(mockUserId);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
|
||||||
|
PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL,
|
||||||
|
null,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createPinKeyEncryptedUserKey()", () => {
|
||||||
|
it("should throw an error if a userKey is not provided", async () => {
|
||||||
|
await expect(
|
||||||
|
sut.createPinKeyEncryptedUserKey(mockPin, undefined, mockUserId),
|
||||||
|
).rejects.toThrow("No UserKey provided. Cannot create pinKeyEncryptedUserKey.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a pinKeyEncryptedUserKey", async () => {
|
||||||
|
// Arrange
|
||||||
|
sut.makePinKey = jest.fn().mockResolvedValue(mockPinKey);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.createPinKeyEncryptedUserKey(mockPin, mockUserKey, mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(encryptService.encrypt).toHaveBeenCalledWith(mockUserKey.key, mockPinKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("storePinKeyEncryptedUserKey", () => {
|
||||||
|
it("should store a pinKeyEncryptedUserKey (persistent version) when 'storeAsEphemeral' is false", async () => {
|
||||||
|
// Arrange
|
||||||
|
const storeAsEphemeral = false;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.storePinKeyEncryptedUserKey(
|
||||||
|
pinKeyEncryptedUserKeyPersistant,
|
||||||
|
storeAsEphemeral,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
|
||||||
|
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||||
|
pinKeyEncryptedUserKeyPersistant.encryptedString,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should store a pinKeyEncryptedUserKeyEphemeral when 'storeAsEphemeral' is true", async () => {
|
||||||
|
// Arrange
|
||||||
|
const storeAsEphemeral = true;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.storePinKeyEncryptedUserKey(
|
||||||
|
pinKeyEncryptedUserKeyEphemeral,
|
||||||
|
storeAsEphemeral,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
|
||||||
|
PIN_KEY_ENCRYPTED_USER_KEY_EPHEMERAL,
|
||||||
|
pinKeyEncryptedUserKeyEphemeral.encryptedString,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("userKeyEncryptedPin methods", () => {
|
||||||
|
describe("getUserKeyEncryptedPin()", () => {
|
||||||
|
it("should get the userKeyEncryptedPin of the specified userId", async () => {
|
||||||
|
await sut.getUserKeyEncryptedPin(mockUserId);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(
|
||||||
|
USER_KEY_ENCRYPTED_PIN,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setUserKeyEncryptedPin()", () => {
|
||||||
|
it("should set the userKeyEncryptedPin of the specified userId", async () => {
|
||||||
|
await sut.setUserKeyEncryptedPin(mockUserKeyEncryptedPin, mockUserId);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
|
||||||
|
USER_KEY_ENCRYPTED_PIN,
|
||||||
|
mockUserKeyEncryptedPin.encryptedString,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("clearUserKeyEncryptedPin()", () => {
|
||||||
|
it("should clear the pinKeyEncryptedUserKey of the specified userId", async () => {
|
||||||
|
await sut.clearUserKeyEncryptedPin(mockUserId);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
|
||||||
|
USER_KEY_ENCRYPTED_PIN,
|
||||||
|
null,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("createUserKeyEncryptedPin()", () => {
|
||||||
|
it("should throw an error if a userKey is not provided", async () => {
|
||||||
|
await expect(sut.createUserKeyEncryptedPin(mockPin, undefined)).rejects.toThrow(
|
||||||
|
"No UserKey provided. Cannot create userKeyEncryptedPin.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a userKeyEncryptedPin from the provided PIN and userKey", async () => {
|
||||||
|
encryptService.encrypt.mockResolvedValue(mockUserKeyEncryptedPin);
|
||||||
|
|
||||||
|
const result = await sut.createUserKeyEncryptedPin(mockPin, mockUserKey);
|
||||||
|
|
||||||
|
expect(encryptService.encrypt).toHaveBeenCalledWith(mockPin, mockUserKey);
|
||||||
|
expect(result).toEqual(mockUserKeyEncryptedPin);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("oldPinKeyEncryptedMasterKey methods", () => {
|
||||||
|
describe("getOldPinKeyEncryptedMasterKey()", () => {
|
||||||
|
it("should get the oldPinKeyEncryptedMasterKey of the specified userId", async () => {
|
||||||
|
await sut.getOldPinKeyEncryptedMasterKey(mockUserId);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.getUserState$).toHaveBeenCalledWith(
|
||||||
|
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("clearOldPinKeyEncryptedMasterKey()", () => {
|
||||||
|
it("should clear the oldPinKeyEncryptedMasterKey of the specified userId", async () => {
|
||||||
|
await sut.clearOldPinKeyEncryptedMasterKey(mockUserId);
|
||||||
|
|
||||||
|
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
|
||||||
|
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
|
||||||
|
null,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("makePinKey()", () => {
|
||||||
|
it("should make a PinKey", async () => {
|
||||||
|
// Arrange
|
||||||
|
keyGenerationService.deriveKeyFromPassword.mockResolvedValue(mockPinKey);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.makePinKey(mockPin, mockUserEmail, DEFAULT_KDF_CONFIG);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(keyGenerationService.deriveKeyFromPassword).toHaveBeenCalledWith(
|
||||||
|
mockPin,
|
||||||
|
mockUserEmail,
|
||||||
|
DEFAULT_KDF_CONFIG,
|
||||||
|
);
|
||||||
|
expect(keyGenerationService.stretchKey).toHaveBeenCalledWith(mockPinKey);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getPinLockType()", () => {
|
||||||
|
it("should return 'PERSISTENT' if a pinKeyEncryptedUserKey (persistent version) is found", async () => {
|
||||||
|
// Arrange
|
||||||
|
sut.getUserKeyEncryptedPin = jest.fn().mockResolvedValue(null);
|
||||||
|
sut.getPinKeyEncryptedUserKeyPersistent = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(pinKeyEncryptedUserKeyPersistant);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await sut.getPinLockType(mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe("PERSISTENT");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 'PERSISTENT' if an old oldPinKeyEncryptedMasterKey is found", async () => {
|
||||||
|
// Arrange
|
||||||
|
sut.getUserKeyEncryptedPin = jest.fn().mockResolvedValue(null);
|
||||||
|
sut.getPinKeyEncryptedUserKeyPersistent = jest.fn().mockResolvedValue(null);
|
||||||
|
sut.getOldPinKeyEncryptedMasterKey = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(oldPinKeyEncryptedMasterKeyPreMigrationPersistent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await sut.getPinLockType(mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe("PERSISTENT");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 'EPHEMERAL' if neither a pinKeyEncryptedUserKey (persistent version) nor an old oldPinKeyEncryptedMasterKey are found, but a userKeyEncryptedPin is found", async () => {
|
||||||
|
// Arrange
|
||||||
|
sut.getUserKeyEncryptedPin = jest.fn().mockResolvedValue(mockUserKeyEncryptedPin);
|
||||||
|
sut.getPinKeyEncryptedUserKeyPersistent = jest.fn().mockResolvedValue(null);
|
||||||
|
sut.getOldPinKeyEncryptedMasterKey = jest.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await sut.getPinLockType(mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe("EPHEMERAL");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 'DISABLED' if ALL three of these are NOT found: userKeyEncryptedPin, pinKeyEncryptedUserKey (persistent version), oldPinKeyEncryptedMasterKey", async () => {
|
||||||
|
// Arrange
|
||||||
|
sut.getUserKeyEncryptedPin = jest.fn().mockResolvedValue(null);
|
||||||
|
sut.getPinKeyEncryptedUserKeyPersistent = jest.fn().mockResolvedValue(null);
|
||||||
|
sut.getOldPinKeyEncryptedMasterKey = jest.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await sut.getPinLockType(mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBe("DISABLED");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isPinSet()", () => {
|
||||||
|
it.each(["PERSISTENT", "EPHEMERAL"])(
|
||||||
|
"should return true if the user PinLockType is '%s'",
|
||||||
|
async () => {
|
||||||
|
// Arrange
|
||||||
|
sut.getPinLockType = jest.fn().mockResolvedValue("PERSISTENT");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await sut.isPinSet(mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should return false if the user PinLockType is 'DISABLED'", async () => {
|
||||||
|
// Arrange
|
||||||
|
sut.getPinLockType = jest.fn().mockResolvedValue("DISABLED");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await sut.isPinSet(mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decryptUserKeyWithPin()", () => {
|
||||||
|
async function setupDecryptUserKeyWithPinMocks(
|
||||||
|
pinLockType: PinLockType,
|
||||||
|
migrationStatus: "PRE" | "POST" = "POST",
|
||||||
|
) {
|
||||||
|
sut.getPinLockType = jest.fn().mockResolvedValue(pinLockType);
|
||||||
|
|
||||||
|
mockPinEncryptedKeyDataByPinLockType(pinLockType, migrationStatus);
|
||||||
|
|
||||||
|
kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG);
|
||||||
|
|
||||||
|
if (pinLockType === "PERSISTENT" && migrationStatus === "PRE") {
|
||||||
|
await mockDecryptAndMigrateOldPinKeyEncryptedMasterKeyFn();
|
||||||
|
} else {
|
||||||
|
mockDecryptUserKeyFn();
|
||||||
|
}
|
||||||
|
|
||||||
|
sut.getUserKeyEncryptedPin = jest.fn().mockResolvedValue(mockUserKeyEncryptedPin);
|
||||||
|
encryptService.decryptToUtf8.mockResolvedValue(mockPin);
|
||||||
|
cryptoFunctionService.compareFast.calledWith(mockPin, "1234").mockResolvedValue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mockDecryptAndMigrateOldPinKeyEncryptedMasterKeyFn() {
|
||||||
|
sut.makePinKey = jest.fn().mockResolvedValue(mockPinKey);
|
||||||
|
encryptService.decryptToBytes.mockResolvedValue(mockMasterKey.key);
|
||||||
|
|
||||||
|
stateService.getEncryptedCryptoSymmetricKey.mockResolvedValue(mockUserKey.keyB64);
|
||||||
|
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(mockUserKey);
|
||||||
|
|
||||||
|
sut.createPinKeyEncryptedUserKey = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(pinKeyEncryptedUserKeyPersistant);
|
||||||
|
|
||||||
|
await sut.storePinKeyEncryptedUserKey(pinKeyEncryptedUserKeyPersistant, false, mockUserId);
|
||||||
|
|
||||||
|
sut.createUserKeyEncryptedPin = jest.fn().mockResolvedValue(mockUserKeyEncryptedPin);
|
||||||
|
await sut.setUserKeyEncryptedPin(mockUserKeyEncryptedPin, mockUserId);
|
||||||
|
|
||||||
|
await sut.clearOldPinKeyEncryptedMasterKey(mockUserId);
|
||||||
|
|
||||||
|
await stateService.setCryptoMasterKeyBiometric(null, { userId: mockUserId });
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockDecryptUserKeyFn() {
|
||||||
|
sut.getPinKeyEncryptedUserKeyPersistent = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(pinKeyEncryptedUserKeyPersistant);
|
||||||
|
sut.makePinKey = jest.fn().mockResolvedValue(mockPinKey);
|
||||||
|
encryptService.decryptToBytes.mockResolvedValue(mockUserKey.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockPinEncryptedKeyDataByPinLockType(
|
||||||
|
pinLockType: PinLockType,
|
||||||
|
migrationStatus: "PRE" | "POST" = "POST",
|
||||||
|
) {
|
||||||
|
switch (pinLockType) {
|
||||||
|
case "PERSISTENT":
|
||||||
|
sut.getPinKeyEncryptedUserKeyPersistent = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(pinKeyEncryptedUserKeyPersistant);
|
||||||
|
|
||||||
|
if (migrationStatus === "PRE") {
|
||||||
|
sut.getOldPinKeyEncryptedMasterKey = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(oldPinKeyEncryptedMasterKeyPreMigrationPersistent);
|
||||||
|
} else {
|
||||||
|
sut.getOldPinKeyEncryptedMasterKey = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(oldPinKeyEncryptedMasterKeyPostMigration); // null
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "EPHEMERAL":
|
||||||
|
sut.getPinKeyEncryptedUserKeyEphemeral = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(pinKeyEncryptedUserKeyEphemeral);
|
||||||
|
|
||||||
|
break;
|
||||||
|
case "DISABLED":
|
||||||
|
// no mocking required. Error should be thrown
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testCases: { pinLockType: PinLockType; migrationStatus: "PRE" | "POST" }[] = [
|
||||||
|
{ pinLockType: "PERSISTENT", migrationStatus: "PRE" },
|
||||||
|
{ pinLockType: "PERSISTENT", migrationStatus: "POST" },
|
||||||
|
{ pinLockType: "EPHEMERAL", migrationStatus: "POST" },
|
||||||
|
];
|
||||||
|
|
||||||
|
testCases.forEach(({ pinLockType, migrationStatus }) => {
|
||||||
|
describe(`given a ${pinLockType} PIN (${migrationStatus} migration)`, () => {
|
||||||
|
if (pinLockType === "PERSISTENT" && migrationStatus === "PRE") {
|
||||||
|
it("should clear the oldPinKeyEncryptedMasterKey from state", async () => {
|
||||||
|
// Arrange
|
||||||
|
await setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.decryptUserKeyWithPin(mockPin, mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
|
||||||
|
OLD_PIN_KEY_ENCRYPTED_MASTER_KEY,
|
||||||
|
null,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set the new pinKeyEncrypterUserKeyPersistent to state", async () => {
|
||||||
|
// Arrange
|
||||||
|
await setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sut.decryptUserKeyWithPin(mockPin, mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(stateProvider.mock.setUserState).toHaveBeenCalledWith(
|
||||||
|
PIN_KEY_ENCRYPTED_USER_KEY_PERSISTENT,
|
||||||
|
pinKeyEncryptedUserKeyPersistant.encryptedString,
|
||||||
|
mockUserId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it(`should successfully decrypt and return user key when using a valid PIN`, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await sut.decryptUserKeyWithPin(mockPin, mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(mockUserKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should return null when PIN is incorrect and user key cannot be decrypted`, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus);
|
||||||
|
sut.decryptUserKeyWithPin = jest.fn().mockResolvedValue(null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await sut.decryptUserKeyWithPin(mockPin, mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
// not sure if this is a realistic scenario but going to test it anyway
|
||||||
|
it(`should return null when PIN doesn't match after successful user key decryption`, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setupDecryptUserKeyWithPinMocks(pinLockType, migrationStatus);
|
||||||
|
encryptService.decryptToUtf8.mockResolvedValue("9999"); // non matching PIN
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await sut.decryptUserKeyWithPin(mockPin, mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should return null when pin is disabled`, async () => {
|
||||||
|
// Arrange
|
||||||
|
await setupDecryptUserKeyWithPinMocks("DISABLED");
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await sut.decryptUserKeyWithPin(mockPin, mockUserId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test helpers
|
||||||
|
function randomBytes(length: number): Uint8Array {
|
||||||
|
return new Uint8Array(Array.from({ length }, (_, k) => k % 255));
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||||
import { PinLockType } from "../../services/vault-timeout/vault-timeout-settings.service";
|
|
||||||
|
|
||||||
export abstract class VaultTimeoutSettingsService {
|
export abstract class VaultTimeoutSettingsService {
|
||||||
/**
|
/**
|
||||||
|
@ -38,13 +37,6 @@ export abstract class VaultTimeoutSettingsService {
|
||||||
*/
|
*/
|
||||||
vaultTimeoutAction$: (userId?: string) => Observable<VaultTimeoutAction>;
|
vaultTimeoutAction$: (userId?: string) => Observable<VaultTimeoutAction>;
|
||||||
|
|
||||||
/**
|
|
||||||
* Has the user enabled unlock with Pin.
|
|
||||||
* @param userId The user id to check. If not provided, the current user is used
|
|
||||||
* @returns PinLockType
|
|
||||||
*/
|
|
||||||
isPinLockSet: (userId?: string) => Promise<PinLockType>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Has the user enabled unlock with Biometric.
|
* Has the user enabled unlock with Biometric.
|
||||||
* @param userId The user id to check. If not provided, the current user is used
|
* @param userId The user id to check. If not provided, the current user is used
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { EncString } from "../../platform/models/domain/enc-string";
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { MasterKey } from "../../types/key";
|
import { MasterKey, UserKey } from "../../types/key";
|
||||||
import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
|
||||||
|
|
||||||
export abstract class MasterPasswordServiceAbstraction {
|
export abstract class MasterPasswordServiceAbstraction {
|
||||||
|
@ -30,6 +30,20 @@ export abstract class MasterPasswordServiceAbstraction {
|
||||||
* @throws If the user ID is missing.
|
* @throws If the user ID is missing.
|
||||||
*/
|
*/
|
||||||
abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise<EncString>;
|
abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise<EncString>;
|
||||||
|
/**
|
||||||
|
* Decrypts the user key with the provided master key
|
||||||
|
* @param masterKey The user's master key
|
||||||
|
* @param userKey The user's encrypted symmetric key
|
||||||
|
* @param userId The desired user
|
||||||
|
* @throws If either the MasterKey or UserKey are not resolved, or if the UserKey encryption type
|
||||||
|
* is neither AesCbc256_B64 nor AesCbc256_HmacSha256_B64
|
||||||
|
* @returns The user key
|
||||||
|
*/
|
||||||
|
abstract decryptUserKeyWithMasterKey: (
|
||||||
|
masterKey: MasterKey,
|
||||||
|
userKey?: EncString,
|
||||||
|
userId?: string,
|
||||||
|
) => Promise<UserKey>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction {
|
export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ReplaySubject, Observable } from "rxjs";
|
||||||
|
|
||||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { MasterKey } from "../../../types/key";
|
import { MasterKey, UserKey } from "../../../types/key";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||||
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
|
||||||
|
|
||||||
|
@ -61,4 +61,12 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
|
||||||
setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> {
|
setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> {
|
||||||
return this.mock.setForceSetPasswordReason(reason, userId);
|
return this.mock.setForceSetPasswordReason(reason, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
decryptUserKeyWithMasterKey(
|
||||||
|
masterKey: MasterKey,
|
||||||
|
userKey?: EncString,
|
||||||
|
userId?: string,
|
||||||
|
): Promise<UserKey> {
|
||||||
|
return this.mock.decryptUserKeyWithMasterKey(masterKey, userKey, userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import { firstValueFrom, map, Observable } from "rxjs";
|
import { firstValueFrom, map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
|
||||||
|
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
|
||||||
|
import { StateService } from "../../../platform/abstractions/state.service";
|
||||||
|
import { EncryptionType } from "../../../platform/enums";
|
||||||
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
|
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
|
||||||
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||||
import {
|
import {
|
||||||
|
@ -9,7 +13,7 @@ import {
|
||||||
UserKeyDefinition,
|
UserKeyDefinition,
|
||||||
} from "../../../platform/state";
|
} from "../../../platform/state";
|
||||||
import { UserId } from "../../../types/guid";
|
import { UserId } from "../../../types/guid";
|
||||||
import { MasterKey } from "../../../types/key";
|
import { MasterKey, UserKey } from "../../../types/key";
|
||||||
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
|
||||||
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
|
||||||
|
|
||||||
|
@ -46,7 +50,12 @@ const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition<ForceSetPasswordReason>(
|
||||||
);
|
);
|
||||||
|
|
||||||
export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction {
|
export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction {
|
||||||
constructor(private stateProvider: StateProvider) {}
|
constructor(
|
||||||
|
private stateProvider: StateProvider,
|
||||||
|
private stateService: StateService,
|
||||||
|
private keyGenerationService: KeyGenerationService,
|
||||||
|
private encryptService: EncryptService,
|
||||||
|
) {}
|
||||||
|
|
||||||
masterKey$(userId: UserId): Observable<MasterKey> {
|
masterKey$(userId: UserId): Observable<MasterKey> {
|
||||||
if (userId == null) {
|
if (userId == null) {
|
||||||
|
@ -137,4 +146,48 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
||||||
}
|
}
|
||||||
await this.stateProvider.getUser(userId, FORCE_SET_PASSWORD_REASON).update((_) => reason);
|
await this.stateProvider.getUser(userId, FORCE_SET_PASSWORD_REASON).update((_) => reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async decryptUserKeyWithMasterKey(
|
||||||
|
masterKey: MasterKey,
|
||||||
|
userKey?: EncString,
|
||||||
|
userId?: UserId,
|
||||||
|
): Promise<UserKey> {
|
||||||
|
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||||
|
userKey ??= await this.getMasterKeyEncryptedUserKey(userId);
|
||||||
|
masterKey ??= await firstValueFrom(this.masterKey$(userId));
|
||||||
|
|
||||||
|
if (masterKey == null) {
|
||||||
|
throw new Error("No master key found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try one more way to get the user key if it still wasn't found.
|
||||||
|
if (userKey == null) {
|
||||||
|
const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({
|
||||||
|
userId: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (deprecatedKey == null) {
|
||||||
|
throw new Error("No encrypted user key found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
userKey = new EncString(deprecatedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
let decUserKey: Uint8Array;
|
||||||
|
|
||||||
|
if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
|
||||||
|
decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey);
|
||||||
|
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
|
||||||
|
const newKey = await this.keyGenerationService.stretchKey(masterKey);
|
||||||
|
decUserKey = await this.encryptService.decryptToBytes(userKey, newKey);
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported encryption type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decUserKey == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SymmetricCryptoKey(decUserKey) as UserKey;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
|
||||||
|
|
||||||
import { PinCryptoServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin-crypto.service.abstraction";
|
import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction";
|
||||||
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||||
|
@ -44,7 +44,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private userVerificationApiService: UserVerificationApiServiceAbstraction,
|
private userVerificationApiService: UserVerificationApiServiceAbstraction,
|
||||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||||
private pinCryptoService: PinCryptoServiceAbstraction,
|
private pinService: PinServiceAbstraction,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
|
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
|
@ -55,10 +55,11 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||||
verificationType: keyof UserVerificationOptions,
|
verificationType: keyof UserVerificationOptions,
|
||||||
): Promise<UserVerificationOptions> {
|
): Promise<UserVerificationOptions> {
|
||||||
if (verificationType === "client") {
|
if (verificationType === "client") {
|
||||||
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
const [userHasMasterPassword, pinLockType, biometricsLockSet, biometricsUserKeyStored] =
|
const [userHasMasterPassword, pinLockType, biometricsLockSet, biometricsUserKeyStored] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.hasMasterPasswordAndMasterKeyHash(),
|
this.hasMasterPasswordAndMasterKeyHash(),
|
||||||
this.vaultTimeoutSettingsService.isPinLockSet(),
|
this.pinService.getPinLockType(userId),
|
||||||
this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
||||||
this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric),
|
this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric),
|
||||||
]);
|
]);
|
||||||
|
@ -137,6 +138,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||||
* @param verification User-supplied verification data (OTP, MP, PIN, or biometrics)
|
* @param verification User-supplied verification data (OTP, MP, PIN, or biometrics)
|
||||||
*/
|
*/
|
||||||
async verifyUser(verification: Verification): Promise<boolean> {
|
async verifyUser(verification: Verification): Promise<boolean> {
|
||||||
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
|
|
||||||
if (verificationHasSecret(verification)) {
|
if (verificationHasSecret(verification)) {
|
||||||
this.validateSecretInput(verification);
|
this.validateSecretInput(verification);
|
||||||
}
|
}
|
||||||
|
@ -145,9 +148,9 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||||
case VerificationType.OTP:
|
case VerificationType.OTP:
|
||||||
return this.verifyUserByOTP(verification);
|
return this.verifyUserByOTP(verification);
|
||||||
case VerificationType.MasterPassword:
|
case VerificationType.MasterPassword:
|
||||||
return this.verifyUserByMasterPassword(verification);
|
return this.verifyUserByMasterPassword(verification, userId);
|
||||||
case VerificationType.PIN:
|
case VerificationType.PIN:
|
||||||
return this.verifyUserByPIN(verification);
|
return this.verifyUserByPIN(verification, userId);
|
||||||
case VerificationType.Biometrics:
|
case VerificationType.Biometrics:
|
||||||
return this.verifyUserByBiometrics();
|
return this.verifyUserByBiometrics();
|
||||||
default: {
|
default: {
|
||||||
|
@ -170,8 +173,12 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||||
|
|
||||||
private async verifyUserByMasterPassword(
|
private async verifyUserByMasterPassword(
|
||||||
verification: MasterPasswordVerification,
|
verification: MasterPasswordVerification,
|
||||||
|
userId: UserId,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
if (!userId) {
|
||||||
|
throw new Error("User ID is required. Cannot verify user by master password.");
|
||||||
|
}
|
||||||
|
|
||||||
let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||||
if (!masterKey) {
|
if (!masterKey) {
|
||||||
masterKey = await this.cryptoService.makeMasterKey(
|
masterKey = await this.cryptoService.makeMasterKey(
|
||||||
|
@ -192,8 +199,12 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyUserByPIN(verification: PinVerification): Promise<boolean> {
|
private async verifyUserByPIN(verification: PinVerification, userId: UserId): Promise<boolean> {
|
||||||
const userKey = await this.pinCryptoService.decryptUserKeyWithPin(verification.secret);
|
if (!userId) {
|
||||||
|
throw new Error("User ID is required. Cannot verify user by PIN.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userKey = await this.pinService.decryptUserKeyWithPin(verification.secret, userId);
|
||||||
|
|
||||||
return userKey != null;
|
return userKey != null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ export class WebAuthnLoginPrfCryptoService implements WebAuthnLoginPrfCryptoServ
|
||||||
return (await this.stretchKey(new Uint8Array(prf))) as PrfKey;
|
return (await this.stretchKey(new Uint8Array(prf))) as PrfKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: use keyGenerationService.stretchKey
|
||||||
private async stretchKey(key: Uint8Array): Promise<SymmetricCryptoKey> {
|
private async stretchKey(key: Uint8Array): Promise<SymmetricCryptoKey> {
|
||||||
const newKey = new Uint8Array(64);
|
const newKey = new Uint8Array(64);
|
||||||
const encKey = await this.cryptoFunctionService.hkdfExpand(key, "enc", 32, "sha256");
|
const encKey = await this.cryptoFunctionService.hkdfExpand(key, "enc", 32, "sha256");
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { ProfileProviderOrganizationResponse } from "../../admin-console/models/
|
||||||
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
|
||||||
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
import { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||||
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
|
import { OrganizationId, ProviderId, UserId } from "../../types/guid";
|
||||||
import { UserKey, MasterKey, OrgKey, ProviderKey, PinKey, CipherKey } from "../../types/key";
|
import { UserKey, MasterKey, OrgKey, ProviderKey, CipherKey } from "../../types/key";
|
||||||
import { KeySuffixOptions, HashPurpose } from "../enums";
|
import { KeySuffixOptions, HashPurpose } from "../enums";
|
||||||
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
|
import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
|
||||||
import { EncString } from "../models/domain/enc-string";
|
import { EncString } from "../models/domain/enc-string";
|
||||||
|
@ -139,18 +139,6 @@ export abstract class CryptoService {
|
||||||
masterKey: MasterKey,
|
masterKey: MasterKey,
|
||||||
userKey?: UserKey,
|
userKey?: UserKey,
|
||||||
): Promise<[UserKey, EncString]>;
|
): Promise<[UserKey, EncString]>;
|
||||||
/**
|
|
||||||
* Decrypts the user key with the provided master key
|
|
||||||
* @param masterKey The user's master key
|
|
||||||
* @param userKey The user's encrypted symmetric key
|
|
||||||
* @param userId The desired user
|
|
||||||
* @returns The user key
|
|
||||||
*/
|
|
||||||
abstract decryptUserKeyWithMasterKey(
|
|
||||||
masterKey: MasterKey,
|
|
||||||
userKey?: EncString,
|
|
||||||
userId?: string,
|
|
||||||
): Promise<UserKey>;
|
|
||||||
/**
|
/**
|
||||||
* Creates a master password hash from the user's master password. Can
|
* Creates a master password hash from the user's master password. Can
|
||||||
* be used for local authentication or for server authentication depending
|
* be used for local authentication or for server authentication depending
|
||||||
|
@ -268,13 +256,6 @@ export abstract class CryptoService {
|
||||||
* @throws If the provided key is a null-ish value.
|
* @throws If the provided key is a null-ish value.
|
||||||
*/
|
*/
|
||||||
abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>;
|
abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>;
|
||||||
/**
|
|
||||||
* @param pin The user's pin
|
|
||||||
* @param salt The user's salt
|
|
||||||
* @param kdfConfig The user's kdf config
|
|
||||||
* @returns A key derived from the user's pin
|
|
||||||
*/
|
|
||||||
abstract makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey>;
|
|
||||||
/**
|
/**
|
||||||
* Clears the user's pin keys from storage
|
* Clears the user's pin keys from storage
|
||||||
* Note: This will remove the stored pin and as a result,
|
* Note: This will remove the stored pin and as a result,
|
||||||
|
@ -282,39 +263,6 @@ export abstract class CryptoService {
|
||||||
* @param userId The desired user
|
* @param userId The desired user
|
||||||
*/
|
*/
|
||||||
abstract clearPinKeys(userId?: string): Promise<void>;
|
abstract clearPinKeys(userId?: string): Promise<void>;
|
||||||
/**
|
|
||||||
* Decrypts the user key with their pin
|
|
||||||
* @param pin The user's PIN
|
|
||||||
* @param salt The user's salt
|
|
||||||
* @param kdfConfig The user's KDF config
|
|
||||||
* @param pinProtectedUserKey The user's PIN protected symmetric key, if not provided
|
|
||||||
* it will be retrieved from storage
|
|
||||||
* @returns The decrypted user key
|
|
||||||
*/
|
|
||||||
abstract decryptUserKeyWithPin(
|
|
||||||
pin: string,
|
|
||||||
salt: string,
|
|
||||||
kdfConfig: KdfConfig,
|
|
||||||
protectedKeyCs?: EncString,
|
|
||||||
): Promise<UserKey>;
|
|
||||||
/**
|
|
||||||
* Creates a new Pin key that encrypts the user key instead of the
|
|
||||||
* master key. Clears the old Pin key from state.
|
|
||||||
* @param masterPasswordOnRestart True if Master Password on Restart is enabled
|
|
||||||
* @param pin User's PIN
|
|
||||||
* @param email User's email
|
|
||||||
* @param kdfConfig User's KdfConfig
|
|
||||||
* @param oldPinKey The old Pin key from state (retrieved from different
|
|
||||||
* places depending on if Master Password on Restart was enabled)
|
|
||||||
* @returns The user key
|
|
||||||
*/
|
|
||||||
abstract decryptAndMigrateOldPinKey(
|
|
||||||
masterPasswordOnRestart: boolean,
|
|
||||||
pin: string,
|
|
||||||
email: string,
|
|
||||||
kdfConfig: KdfConfig,
|
|
||||||
oldPinKey: EncString,
|
|
||||||
): Promise<UserKey>;
|
|
||||||
/**
|
/**
|
||||||
* @param keyMaterial The key material to derive the send key from
|
* @param keyMaterial The key material to derive the send key from
|
||||||
* @returns A new send key
|
* @returns A new send key
|
||||||
|
@ -358,16 +306,6 @@ export abstract class CryptoService {
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
privateKey: EncString;
|
privateKey: EncString;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead.
|
|
||||||
*/
|
|
||||||
abstract decryptMasterKeyWithPin(
|
|
||||||
pin: string,
|
|
||||||
salt: string,
|
|
||||||
kdfConfig: KdfConfig,
|
|
||||||
protectedKeyCs?: EncString,
|
|
||||||
): Promise<MasterKey>;
|
|
||||||
/**
|
/**
|
||||||
* Previously, the master key was used for any additional key like the biometrics or pin key.
|
* Previously, the master key was used for any additional key like the biometrics or pin key.
|
||||||
* We have switched to using the user key for these purposes. This method is for clearing the state
|
* We have switched to using the user key for these purposes. This method is for clearing the state
|
||||||
|
|
|
@ -53,4 +53,11 @@ export abstract class KeyGenerationService {
|
||||||
salt: string | Uint8Array,
|
salt: string | Uint8Array,
|
||||||
kdfConfig: KdfConfig,
|
kdfConfig: KdfConfig,
|
||||||
): Promise<SymmetricCryptoKey>;
|
): Promise<SymmetricCryptoKey>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derives a 64 byte key from a 32 byte key using a key derivation function.
|
||||||
|
* @param key 32 byte key.
|
||||||
|
* @returns 64 byte derived key.
|
||||||
|
*/
|
||||||
|
abstract stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/
|
||||||
import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
import { UsernameGeneratorOptions } from "../../tools/generator/username";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { Account } from "../models/domain/account";
|
import { Account } from "../models/domain/account";
|
||||||
import { EncString } from "../models/domain/enc-string";
|
|
||||||
import { StorageOptions } from "../models/domain/storage-options";
|
import { StorageOptions } from "../models/domain/storage-options";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,26 +46,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||||
* Sets the user's biometric key
|
* Sets the user's biometric key
|
||||||
*/
|
*/
|
||||||
setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
|
setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
|
||||||
/**
|
|
||||||
* Gets the user key encrypted by the Pin key.
|
|
||||||
* Used when Lock with MP on Restart is disabled
|
|
||||||
*/
|
|
||||||
getPinKeyEncryptedUserKey: (options?: StorageOptions) => Promise<EncString>;
|
|
||||||
/**
|
|
||||||
* Sets the user key encrypted by the Pin key.
|
|
||||||
* Used when Lock with MP on Restart is disabled
|
|
||||||
*/
|
|
||||||
setPinKeyEncryptedUserKey: (value: EncString, options?: StorageOptions) => Promise<void>;
|
|
||||||
/**
|
|
||||||
* Gets the ephemeral version of the user key encrypted by the Pin key.
|
|
||||||
* Used when Lock with MP on Restart is enabled
|
|
||||||
*/
|
|
||||||
getPinKeyEncryptedUserKeyEphemeral: (options?: StorageOptions) => Promise<EncString>;
|
|
||||||
/**
|
|
||||||
* Sets the ephemeral version of the user key encrypted by the Pin key.
|
|
||||||
* Used when Lock with MP on Restart is enabled
|
|
||||||
*/
|
|
||||||
setPinKeyEncryptedUserKeyEphemeral: (value: EncString, options?: StorageOptions) => Promise<void>;
|
|
||||||
/**
|
/**
|
||||||
* @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService
|
* @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService
|
||||||
*/
|
*/
|
||||||
|
@ -101,14 +80,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||||
value: GeneratedPasswordHistory[],
|
value: GeneratedPasswordHistory[],
|
||||||
options?: StorageOptions,
|
options?: StorageOptions,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
/**
|
|
||||||
* @deprecated For migration purposes only, use getDecryptedUserKeyPin instead
|
|
||||||
*/
|
|
||||||
getDecryptedPinProtected: (options?: StorageOptions) => Promise<EncString>;
|
|
||||||
/**
|
|
||||||
* @deprecated For migration purposes only, use setDecryptedUserKeyPin instead
|
|
||||||
*/
|
|
||||||
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
|
|
||||||
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
|
||||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||||
|
@ -127,14 +98,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||||
value: GeneratedPasswordHistory[],
|
value: GeneratedPasswordHistory[],
|
||||||
options?: StorageOptions,
|
options?: StorageOptions,
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
/**
|
|
||||||
* @deprecated For migration purposes only, use getEncryptedUserKeyPin instead
|
|
||||||
*/
|
|
||||||
getEncryptedPinProtected: (options?: StorageOptions) => Promise<string>;
|
|
||||||
/**
|
|
||||||
* @deprecated For migration purposes only, use setEncryptedUserKeyPin instead
|
|
||||||
*/
|
|
||||||
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
|
|
||||||
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
|
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
|
||||||
getLastSync: (options?: StorageOptions) => Promise<string>;
|
getLastSync: (options?: StorageOptions) => Promise<string>;
|
||||||
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
|
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
@ -154,14 +117,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
getGeneratorOptions: (options?: StorageOptions) => Promise<GeneratorOptions>;
|
getGeneratorOptions: (options?: StorageOptions) => Promise<GeneratorOptions>;
|
||||||
setGeneratorOptions: (value: GeneratorOptions, options?: StorageOptions) => Promise<void>;
|
setGeneratorOptions: (value: GeneratorOptions, options?: StorageOptions) => Promise<void>;
|
||||||
/**
|
|
||||||
* Gets the user's Pin, encrypted by the user key
|
|
||||||
*/
|
|
||||||
getProtectedPin: (options?: StorageOptions) => Promise<string>;
|
|
||||||
/**
|
|
||||||
* Sets the user's Pin, encrypted by the user key
|
|
||||||
*/
|
|
||||||
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
|
|
||||||
getUserId: (options?: StorageOptions) => Promise<string>;
|
getUserId: (options?: StorageOptions) => Promise<string>;
|
||||||
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
|
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
|
||||||
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
||||||
|
|
|
@ -1,24 +1,9 @@
|
||||||
import { AccountSettings, EncryptionPair } from "./account";
|
import { AccountSettings } from "./account";
|
||||||
import { EncString } from "./enc-string";
|
|
||||||
|
|
||||||
describe("AccountSettings", () => {
|
describe("AccountSettings", () => {
|
||||||
describe("fromJSON", () => {
|
describe("fromJSON", () => {
|
||||||
it("should deserialize to an instance of itself", () => {
|
it("should deserialize to an instance of itself", () => {
|
||||||
expect(AccountSettings.fromJSON(JSON.parse("{}"))).toBeInstanceOf(AccountSettings);
|
expect(AccountSettings.fromJSON(JSON.parse("{}"))).toBeInstanceOf(AccountSettings);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should deserialize pinProtected", () => {
|
|
||||||
const accountSettings = new AccountSettings();
|
|
||||||
accountSettings.pinProtected = EncryptionPair.fromJSON<string, EncString>({
|
|
||||||
encrypted: "encrypted",
|
|
||||||
decrypted: "3.data",
|
|
||||||
});
|
|
||||||
const jsonObj = JSON.parse(JSON.stringify(accountSettings));
|
|
||||||
const actual = AccountSettings.fromJSON(jsonObj);
|
|
||||||
|
|
||||||
expect(actual.pinProtected).toBeInstanceOf(EncryptionPair);
|
|
||||||
expect(actual.pinProtected.encrypted).toEqual("encrypted");
|
|
||||||
expect(actual.pinProtected.decrypted.encryptedString).toEqual("3.data");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { DeepJsonify } from "../../../types/deep-jsonify";
|
||||||
import { KdfType } from "../../enums";
|
import { KdfType } from "../../enums";
|
||||||
import { Utils } from "../../misc/utils";
|
import { Utils } from "../../misc/utils";
|
||||||
|
|
||||||
import { EncryptedString, EncString } from "./enc-string";
|
|
||||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||||
|
|
||||||
export class EncryptionPair<TEncrypted, TDecrypted> {
|
export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||||
|
@ -148,26 +147,15 @@ export class AccountSettings {
|
||||||
passwordGenerationOptions?: PasswordGeneratorOptions;
|
passwordGenerationOptions?: PasswordGeneratorOptions;
|
||||||
usernameGenerationOptions?: UsernameGeneratorOptions;
|
usernameGenerationOptions?: UsernameGeneratorOptions;
|
||||||
generatorOptions?: GeneratorOptions;
|
generatorOptions?: GeneratorOptions;
|
||||||
pinKeyEncryptedUserKey?: EncryptedString;
|
|
||||||
pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
|
|
||||||
protectedPin?: string;
|
|
||||||
vaultTimeout?: number;
|
vaultTimeout?: number;
|
||||||
vaultTimeoutAction?: string = "lock";
|
vaultTimeoutAction?: string = "lock";
|
||||||
|
|
||||||
/** @deprecated July 2023, left for migration purposes*/
|
|
||||||
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
|
||||||
|
|
||||||
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
||||||
if (obj == null) {
|
if (obj == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign(new AccountSettings(), obj, {
|
return Object.assign(new AccountSettings(), obj);
|
||||||
pinProtected: EncryptionPair.fromJSON<string, EncString>(
|
|
||||||
obj?.pinProtected,
|
|
||||||
EncString.fromJSON,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
import { firstValueFrom, of, tap } from "rxjs";
|
import { firstValueFrom, of, tap } from "rxjs";
|
||||||
|
|
||||||
|
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
|
||||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||||
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
||||||
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
import { FakeStateProvider } from "../../../spec/fake-state-provider";
|
||||||
|
@ -8,7 +9,7 @@ import { KdfConfigService } from "../../auth/abstractions/kdf-config.service";
|
||||||
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
|
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
|
||||||
import { CsprngArray } from "../../types/csprng";
|
import { CsprngArray } from "../../types/csprng";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { UserKey, MasterKey, PinKey } from "../../types/key";
|
import { UserKey, MasterKey } from "../../types/key";
|
||||||
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "../abstractions/crypto-function.service";
|
||||||
import { EncryptService } from "../abstractions/encrypt.service";
|
import { EncryptService } from "../abstractions/encrypt.service";
|
||||||
import { KeyGenerationService } from "../abstractions/key-generation.service";
|
import { KeyGenerationService } from "../abstractions/key-generation.service";
|
||||||
|
@ -32,6 +33,7 @@ import {
|
||||||
describe("cryptoService", () => {
|
describe("cryptoService", () => {
|
||||||
let cryptoService: CryptoService;
|
let cryptoService: CryptoService;
|
||||||
|
|
||||||
|
const pinService = mock<PinServiceAbstraction>();
|
||||||
const keyGenerationService = mock<KeyGenerationService>();
|
const keyGenerationService = mock<KeyGenerationService>();
|
||||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||||
const encryptService = mock<EncryptService>();
|
const encryptService = mock<EncryptService>();
|
||||||
|
@ -51,6 +53,7 @@ describe("cryptoService", () => {
|
||||||
stateProvider = new FakeStateProvider(accountService);
|
stateProvider = new FakeStateProvider(accountService);
|
||||||
|
|
||||||
cryptoService = new CryptoService(
|
cryptoService = new CryptoService(
|
||||||
|
pinService,
|
||||||
masterPasswordService,
|
masterPasswordService,
|
||||||
keyGenerationService,
|
keyGenerationService,
|
||||||
cryptoFunctionService,
|
cryptoFunctionService,
|
||||||
|
@ -251,60 +254,50 @@ describe("cryptoService", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Pin Key refresh", () => {
|
describe("Pin Key refresh", () => {
|
||||||
let cryptoSvcMakePinKey: jest.SpyInstance;
|
const mockPinKeyEncryptedUserKey = new EncString(
|
||||||
const protectedPin =
|
"2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
|
||||||
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=";
|
);
|
||||||
let encPin: EncString;
|
const mockUserKeyEncryptedPin = new EncString(
|
||||||
|
"2.BBBw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
|
||||||
|
);
|
||||||
|
|
||||||
beforeEach(() => {
|
it("sets a pinKeyEncryptedUserKeyPersistent if a userKeyEncryptedPin and pinKeyEncryptedUserKey is set", async () => {
|
||||||
cryptoSvcMakePinKey = jest.spyOn(cryptoService, "makePinKey");
|
pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
|
||||||
cryptoSvcMakePinKey.mockResolvedValue(new SymmetricCryptoKey(new Uint8Array(64)) as PinKey);
|
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
|
||||||
encPin = new EncString(
|
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(
|
||||||
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
|
mockPinKeyEncryptedUserKey,
|
||||||
);
|
|
||||||
encryptService.encrypt.mockResolvedValue(encPin);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("sets a UserKeyPin if a ProtectedPin and UserKeyPin is set", async () => {
|
|
||||||
stateService.getProtectedPin.mockResolvedValue(protectedPin);
|
|
||||||
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(
|
|
||||||
new EncString(
|
|
||||||
"2.OdGNE3L23GaDZGvu9h2Brw==|/OAcNnrYwu0rjiv8+RUr3Tc+Ef8fV035Tm1rbTxfEuC+2LZtiCAoIvHIZCrM/V1PWnb/pHO2gh9+Koks04YhX8K29ED4FzjeYP8+YQD/dWo=|+12xTcIK/UVRsOyawYudPMHb6+lCHeR2Peq1pQhPm0A=",
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await cryptoService.setUserKey(mockUserKey, mockUserId);
|
await cryptoService.setUserKey(mockUserKey, mockUserId);
|
||||||
|
|
||||||
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(expect.any(EncString), {
|
expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||||
userId: mockUserId,
|
mockPinKeyEncryptedUserKey,
|
||||||
});
|
false,
|
||||||
});
|
mockUserId,
|
||||||
|
|
||||||
it("sets a PinKeyEphemeral if a ProtectedPin is set, but a UserKeyPin is not set", async () => {
|
|
||||||
stateService.getProtectedPin.mockResolvedValue(protectedPin);
|
|
||||||
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
|
|
||||||
|
|
||||||
await cryptoService.setUserKey(mockUserKey, mockUserId);
|
|
||||||
|
|
||||||
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(
|
|
||||||
expect.any(EncString),
|
|
||||||
{
|
|
||||||
userId: mockUserId,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clears the UserKeyPin and UserKeyPinEphemeral if the ProtectedPin is not set", async () => {
|
it("sets a pinKeyEncryptedUserKeyEphemeral if a userKeyEncryptedPin is set, but a pinKeyEncryptedUserKey is not set", async () => {
|
||||||
stateService.getProtectedPin.mockResolvedValue(null);
|
pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
|
||||||
|
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
|
||||||
|
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(null);
|
||||||
|
|
||||||
await cryptoService.setUserKey(mockUserKey, mockUserId);
|
await cryptoService.setUserKey(mockUserKey, mockUserId);
|
||||||
|
|
||||||
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(null, {
|
expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||||
userId: mockUserId,
|
mockPinKeyEncryptedUserKey,
|
||||||
});
|
true,
|
||||||
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(null, {
|
mockUserId,
|
||||||
userId: mockUserId,
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("clears the pinKeyEncryptedUserKeyPersistent and pinKeyEncryptedUserKeyEphemeral if the UserKeyEncryptedPin is not set", async () => {
|
||||||
|
pinService.getUserKeyEncryptedPin.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await cryptoService.setUserKey(mockUserKey, mockUserId);
|
||||||
|
|
||||||
|
expect(pinService.clearPinKeyEncryptedUserKeyPersistent).toHaveBeenCalledWith(mockUserId);
|
||||||
|
expect(pinService.clearPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(mockUserId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as bigInt from "big-integer";
|
import * as bigInt from "big-integer";
|
||||||
import { Observable, filter, firstValueFrom, map } from "rxjs";
|
import { Observable, filter, firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
|
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
|
||||||
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
|
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
|
||||||
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
||||||
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
||||||
|
@ -17,7 +18,6 @@ import {
|
||||||
UserKey,
|
UserKey,
|
||||||
MasterKey,
|
MasterKey,
|
||||||
ProviderKey,
|
ProviderKey,
|
||||||
PinKey,
|
|
||||||
CipherKey,
|
CipherKey,
|
||||||
UserPrivateKey,
|
UserPrivateKey,
|
||||||
UserPublicKey,
|
UserPublicKey,
|
||||||
|
@ -74,6 +74,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||||
readonly everHadUserKey$: Observable<boolean>;
|
readonly everHadUserKey$: Observable<boolean>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
protected pinService: PinServiceAbstraction,
|
||||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||||
protected keyGenerationService: KeyGenerationService,
|
protected keyGenerationService: KeyGenerationService,
|
||||||
protected cryptoFunctionService: CryptoFunctionService,
|
protected cryptoFunctionService: CryptoFunctionService,
|
||||||
|
@ -254,7 +255,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||||
if (keySuffix === KeySuffixOptions.Pin) {
|
if (keySuffix === KeySuffixOptions.Pin) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
|
this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
|
||||||
|
@ -303,46 +304,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||||
return await this.buildProtectedSymmetricKey(masterKey, userKey.key);
|
return await this.buildProtectedSymmetricKey(masterKey, userKey.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move to master password service
|
|
||||||
async decryptUserKeyWithMasterKey(
|
|
||||||
masterKey: MasterKey,
|
|
||||||
userKey?: EncString,
|
|
||||||
userId?: UserId,
|
|
||||||
): Promise<UserKey> {
|
|
||||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
|
||||||
userKey ??= await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId);
|
|
||||||
masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
|
||||||
if (masterKey == null) {
|
|
||||||
throw new Error("No master key found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try one more way to get the user key if it still wasn't found.
|
|
||||||
if (userKey == null) {
|
|
||||||
const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({
|
|
||||||
userId: userId,
|
|
||||||
});
|
|
||||||
if (deprecatedKey == null) {
|
|
||||||
throw new Error("No encrypted user key found.");
|
|
||||||
}
|
|
||||||
userKey = new EncString(deprecatedKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
let decUserKey: Uint8Array;
|
|
||||||
if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
|
|
||||||
decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey);
|
|
||||||
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
|
|
||||||
const newKey = await this.stretchKey(masterKey);
|
|
||||||
decUserKey = await this.encryptService.decryptToBytes(userKey, newKey);
|
|
||||||
} else {
|
|
||||||
throw new Error("Unsupported encryption type.");
|
|
||||||
}
|
|
||||||
if (decUserKey == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SymmetricCryptoKey(decUserKey) as UserKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: move to MasterPasswordService
|
// TODO: move to MasterPasswordService
|
||||||
async hashMasterKey(
|
async hashMasterKey(
|
||||||
password: string,
|
password: string,
|
||||||
|
@ -548,53 +509,19 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||||
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
|
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
|
|
||||||
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig);
|
|
||||||
return (await this.stretchKey(pinKey)) as PinKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
async clearPinKeys(userId?: UserId): Promise<void> {
|
async clearPinKeys(userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId });
|
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
|
||||||
await this.stateService.setProtectedPin(null, { userId: userId });
|
if (userId == null) {
|
||||||
|
throw new Error("Cannot clear PIN keys, no user Id resolved.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
|
||||||
|
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||||
|
await this.pinService.clearUserKeyEncryptedPin(userId);
|
||||||
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
|
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptUserKeyWithPin(
|
|
||||||
pin: string,
|
|
||||||
salt: string,
|
|
||||||
kdfConfig: KdfConfig,
|
|
||||||
pinProtectedUserKey?: EncString,
|
|
||||||
): Promise<UserKey> {
|
|
||||||
pinProtectedUserKey ||= await this.stateService.getPinKeyEncryptedUserKey();
|
|
||||||
pinProtectedUserKey ||= await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
|
|
||||||
if (!pinProtectedUserKey) {
|
|
||||||
throw new Error("No PIN protected key found.");
|
|
||||||
}
|
|
||||||
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
|
|
||||||
const userKey = await this.encryptService.decryptToBytes(pinProtectedUserKey, pinKey);
|
|
||||||
return new SymmetricCryptoKey(userKey) as UserKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// only for migration purposes
|
|
||||||
async decryptMasterKeyWithPin(
|
|
||||||
pin: string,
|
|
||||||
salt: string,
|
|
||||||
kdfConfig: KdfConfig,
|
|
||||||
pinProtectedMasterKey?: EncString,
|
|
||||||
): Promise<MasterKey> {
|
|
||||||
if (!pinProtectedMasterKey) {
|
|
||||||
const pinProtectedMasterKeyString = await this.stateService.getEncryptedPinProtected();
|
|
||||||
if (pinProtectedMasterKeyString == null) {
|
|
||||||
throw new Error("No PIN protected key found.");
|
|
||||||
}
|
|
||||||
pinProtectedMasterKey = new EncString(pinProtectedMasterKeyString);
|
|
||||||
}
|
|
||||||
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
|
|
||||||
const masterKey = await this.encryptService.decryptToBytes(pinProtectedMasterKey, pinKey);
|
|
||||||
return new SymmetricCryptoKey(masterKey) as MasterKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
async makeSendKey(keyMaterial: CsprngArray): Promise<SymmetricCryptoKey> {
|
async makeSendKey(keyMaterial: CsprngArray): Promise<SymmetricCryptoKey> {
|
||||||
return await this.keyGenerationService.deriveKeyFromMaterial(
|
return await this.keyGenerationService.deriveKeyFromMaterial(
|
||||||
keyMaterial,
|
keyMaterial,
|
||||||
|
@ -798,6 +725,12 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||||
* @param userId The desired user
|
* @param userId The desired user
|
||||||
*/
|
*/
|
||||||
protected async storeAdditionalKeys(key: UserKey, userId?: UserId) {
|
protected async storeAdditionalKeys(key: UserKey, userId?: UserId) {
|
||||||
|
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||||
|
|
||||||
|
if (userId == null) {
|
||||||
|
throw new Error("Cannot store additional keys, no user Id resolved.");
|
||||||
|
}
|
||||||
|
|
||||||
const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
|
const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
|
||||||
if (storeAuto) {
|
if (storeAuto) {
|
||||||
await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId });
|
await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId });
|
||||||
|
@ -808,37 +741,31 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||||
|
|
||||||
const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId);
|
const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId);
|
||||||
if (storePin) {
|
if (storePin) {
|
||||||
await this.storePinKey(key, userId);
|
// Decrypt userKeyEncryptedPin with user key
|
||||||
|
const pin = await this.encryptService.decryptToUtf8(
|
||||||
|
await this.pinService.getUserKeyEncryptedPin(userId),
|
||||||
|
key,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
|
||||||
|
pin,
|
||||||
|
key,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
const noPreExistingPersistentKey =
|
||||||
|
(await this.pinService.getPinKeyEncryptedUserKeyPersistent(userId)) == null;
|
||||||
|
|
||||||
|
await this.pinService.storePinKeyEncryptedUserKey(
|
||||||
|
pinKeyEncryptedUserKey,
|
||||||
|
noPreExistingPersistentKey,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
// We can't always clear deprecated keys because the pin is only
|
// We can't always clear deprecated keys because the pin is only
|
||||||
// migrated once used to unlock
|
// migrated once used to unlock
|
||||||
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
|
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
|
||||||
} else {
|
} else {
|
||||||
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId });
|
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
|
||||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stores the pin key if needed. If MP on Reset is enabled, stores the
|
|
||||||
* ephemeral version.
|
|
||||||
* @param key The user key
|
|
||||||
*/
|
|
||||||
protected async storePinKey(key: UserKey, userId?: UserId) {
|
|
||||||
const pin = await this.encryptService.decryptToUtf8(
|
|
||||||
new EncString(await this.stateService.getProtectedPin({ userId: userId })),
|
|
||||||
key,
|
|
||||||
);
|
|
||||||
const pinKey = await this.makePinKey(
|
|
||||||
pin,
|
|
||||||
await this.stateService.getEmail({ userId: userId }),
|
|
||||||
await this.kdfConfigService.getKdfConfig(),
|
|
||||||
);
|
|
||||||
const encPin = await this.encryptService.encrypt(key.key, pinKey);
|
|
||||||
|
|
||||||
if ((await this.stateService.getPinKeyEncryptedUserKey({ userId: userId })) != null) {
|
|
||||||
await this.stateService.setPinKeyEncryptedUserKey(encPin, { userId: userId });
|
|
||||||
} else {
|
|
||||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(encPin, { userId: userId });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -851,8 +778,8 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case KeySuffixOptions.Pin: {
|
case KeySuffixOptions.Pin: {
|
||||||
const protectedPin = await this.stateService.getProtectedPin({ userId: userId });
|
const userKeyEncryptedPin = await this.pinService.getUserKeyEncryptedPin(userId);
|
||||||
shouldStoreKey = !!protectedPin;
|
shouldStoreKey = !!userKeyEncryptedPin;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -874,16 +801,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||||
|
|
||||||
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
||||||
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
||||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||||
}
|
|
||||||
|
|
||||||
private async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
|
||||||
const newKey = new Uint8Array(64);
|
|
||||||
const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256");
|
|
||||||
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
|
|
||||||
newKey.set(new Uint8Array(encKey));
|
|
||||||
newKey.set(new Uint8Array(macKey), 32);
|
|
||||||
return new SymmetricCryptoKey(newKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) {
|
private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) {
|
||||||
|
@ -912,7 +830,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||||
): Promise<[T, EncString]> {
|
): Promise<[T, EncString]> {
|
||||||
let protectedSymKey: EncString = null;
|
let protectedSymKey: EncString = null;
|
||||||
if (encryptionKey.key.byteLength === 32) {
|
if (encryptionKey.key.byteLength === 32) {
|
||||||
const stretchedEncryptionKey = await this.stretchKey(encryptionKey);
|
const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey);
|
||||||
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
|
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
|
||||||
} else if (encryptionKey.key.byteLength === 64) {
|
} else if (encryptionKey.key.byteLength === 64) {
|
||||||
protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey);
|
protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey);
|
||||||
|
@ -931,42 +849,10 @@ export class CryptoService implements CryptoServiceAbstraction {
|
||||||
if (keySuffix === KeySuffixOptions.Auto) {
|
if (keySuffix === KeySuffixOptions.Auto) {
|
||||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||||
} else if (keySuffix === KeySuffixOptions.Pin) {
|
} else if (keySuffix === KeySuffixOptions.Pin) {
|
||||||
await this.stateService.setEncryptedPinProtected(null, { userId: userId });
|
await this.pinService.clearOldPinKeyEncryptedMasterKey(userId);
|
||||||
await this.stateService.setDecryptedPinProtected(null, { userId: userId });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async decryptAndMigrateOldPinKey(
|
|
||||||
masterPasswordOnRestart: boolean,
|
|
||||||
pin: string,
|
|
||||||
email: string,
|
|
||||||
kdfConfig: KdfConfig,
|
|
||||||
oldPinKey: EncString,
|
|
||||||
): Promise<UserKey> {
|
|
||||||
// Decrypt
|
|
||||||
const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdfConfig, oldPinKey);
|
|
||||||
const encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey();
|
|
||||||
const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey));
|
|
||||||
// Migrate
|
|
||||||
const pinKey = await this.makePinKey(pin, email, kdfConfig);
|
|
||||||
const pinProtectedKey = await this.encryptService.encrypt(userKey.key, pinKey);
|
|
||||||
if (masterPasswordOnRestart) {
|
|
||||||
await this.stateService.setDecryptedPinProtected(null);
|
|
||||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(pinProtectedKey);
|
|
||||||
} else {
|
|
||||||
await this.stateService.setEncryptedPinProtected(null);
|
|
||||||
await this.stateService.setPinKeyEncryptedUserKey(pinProtectedKey);
|
|
||||||
// We previously only set the protected pin if MP on Restart was enabled
|
|
||||||
// now we set it regardless
|
|
||||||
const encPin = await this.encryptService.encrypt(pin, userKey);
|
|
||||||
await this.stateService.setProtectedPin(encPin.encryptedString);
|
|
||||||
}
|
|
||||||
// This also clears the old Biometrics key since the new Biometrics key will
|
|
||||||
// be created when the user key is set.
|
|
||||||
await this.stateService.setCryptoMasterKeyBiometric(null);
|
|
||||||
return userKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --DEPRECATED METHODS--
|
// --DEPRECATED METHODS--
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -81,4 +81,15 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction {
|
||||||
}
|
}
|
||||||
return new SymmetricCryptoKey(key);
|
return new SymmetricCryptoKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
|
||||||
|
const newKey = new Uint8Array(64);
|
||||||
|
const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256");
|
||||||
|
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
|
||||||
|
|
||||||
|
newKey.set(new Uint8Array(encKey));
|
||||||
|
newKey.set(new Uint8Array(macKey), 32);
|
||||||
|
|
||||||
|
return new SymmetricCryptoKey(newKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { HtmlStorageLocation, StorageLocation } from "../enums";
|
||||||
import { StateFactory } from "../factories/state-factory";
|
import { StateFactory } from "../factories/state-factory";
|
||||||
import { Utils } from "../misc/utils";
|
import { Utils } from "../misc/utils";
|
||||||
import { Account, AccountData, AccountSettings } from "../models/domain/account";
|
import { Account, AccountData, AccountSettings } from "../models/domain/account";
|
||||||
import { EncString } from "../models/domain/enc-string";
|
|
||||||
import { GlobalState } from "../models/domain/global-state";
|
import { GlobalState } from "../models/domain/global-state";
|
||||||
import { State } from "../models/domain/state";
|
import { State } from "../models/domain/state";
|
||||||
import { StorageOptions } from "../models/domain/storage-options";
|
import { StorageOptions } from "../models/domain/storage-options";
|
||||||
|
@ -220,45 +219,6 @@ export class StateService<
|
||||||
await this.saveSecureStorageKey(partialKeys.userBiometricKey, value, options);
|
await this.saveSecureStorageKey(partialKeys.userBiometricKey, value, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPinKeyEncryptedUserKey(options?: StorageOptions): Promise<EncString> {
|
|
||||||
return EncString.fromJSON(
|
|
||||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
|
||||||
?.settings?.pinKeyEncryptedUserKey,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setPinKeyEncryptedUserKey(value: EncString, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.settings.pinKeyEncryptedUserKey = value?.encryptedString;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPinKeyEncryptedUserKeyEphemeral(options?: StorageOptions): Promise<EncString> {
|
|
||||||
return EncString.fromJSON(
|
|
||||||
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
|
|
||||||
?.settings?.pinKeyEncryptedUserKeyEphemeral,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setPinKeyEncryptedUserKeyEphemeral(
|
|
||||||
value: EncString,
|
|
||||||
options?: StorageOptions,
|
|
||||||
): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
account.settings.pinKeyEncryptedUserKeyEphemeral = value?.encryptedString;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Use UserKeyAuto instead
|
* @deprecated Use UserKeyAuto instead
|
||||||
*/
|
*/
|
||||||
|
@ -369,29 +329,6 @@ export class StateService<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use getPinKeyEncryptedUserKeyEphemeral instead
|
|
||||||
*/
|
|
||||||
async getDecryptedPinProtected(options?: StorageOptions): Promise<EncString> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
|
|
||||||
)?.settings?.pinProtected?.decrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use setPinKeyEncryptedUserKeyEphemeral instead
|
|
||||||
*/
|
|
||||||
async setDecryptedPinProtected(value: EncString, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
account.settings.pinProtected.decrypted = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
|
async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
|
||||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||||
if (options?.userId == null) {
|
if (options?.userId == null) {
|
||||||
|
@ -512,23 +449,6 @@ export class StateService<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEncryptedPinProtected(options?: StorageOptions): Promise<string> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
|
||||||
)?.settings?.pinProtected?.encrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setEncryptedPinProtected(value: string, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.settings.pinProtected.encrypted = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getIsAuthenticated(options?: StorageOptions): Promise<boolean> {
|
async getIsAuthenticated(options?: StorageOptions): Promise<boolean> {
|
||||||
return (
|
return (
|
||||||
(await this.tokenService.getAccessToken(options?.userId as UserId)) != null &&
|
(await this.tokenService.getAccessToken(options?.userId as UserId)) != null &&
|
||||||
|
@ -645,23 +565,6 @@ export class StateService<
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProtectedPin(options?: StorageOptions): Promise<string> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
|
||||||
)?.settings?.protectedPin;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setProtectedPin(value: string, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.settings.protectedPin = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUserId(options?: StorageOptions): Promise<string> {
|
async getUserId(options?: StorageOptions): Promise<string> {
|
||||||
return (
|
return (
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { firstValueFrom, map, timeout } from "rxjs";
|
import { firstValueFrom, map, timeout } from "rxjs";
|
||||||
|
|
||||||
|
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
|
||||||
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
|
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
|
||||||
import { AccountService } from "../../auth/abstractions/account.service";
|
import { AccountService } from "../../auth/abstractions/account.service";
|
||||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||||
|
@ -20,6 +21,7 @@ export class SystemService implements SystemServiceAbstraction {
|
||||||
private clearClipboardTimeoutFunction: () => Promise<any> = null;
|
private clearClipboardTimeoutFunction: () => Promise<any> = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private pinService: PinServiceAbstraction,
|
||||||
private messagingService: MessagingService,
|
private messagingService: MessagingService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private reloadCallback: () => Promise<void> = null,
|
private reloadCallback: () => Promise<void> = null,
|
||||||
|
@ -50,10 +52,13 @@ export class SystemService implements SystemServiceAbstraction {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// User has set a PIN, with ask for master password on restart, to protect their vault
|
// If there is an active user, check if they have a pinKeyEncryptedUserKeyEphemeral. If so, prevent process reload upon lock.
|
||||||
const ephemeralPin = await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
if (ephemeralPin != null) {
|
if (userId != null) {
|
||||||
return;
|
const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId);
|
||||||
|
if (ephemeralPin != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cancelProcessReload();
|
this.cancelProcessReload();
|
||||||
|
|
|
@ -35,31 +35,33 @@ export const BILLING_DISK = new StateDefinition("billing", "disk");
|
||||||
|
|
||||||
// Auth
|
// Auth
|
||||||
|
|
||||||
|
export const ACCOUNT_DISK = new StateDefinition("account", "disk");
|
||||||
|
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||||
|
export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", {
|
||||||
|
web: "disk-local",
|
||||||
|
});
|
||||||
|
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
|
||||||
|
export const DEVICE_TRUST_DISK_LOCAL = new StateDefinition("deviceTrust", "disk", {
|
||||||
|
web: "disk-local",
|
||||||
|
});
|
||||||
export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk");
|
export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk");
|
||||||
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
|
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
|
||||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
|
||||||
export const ACCOUNT_DISK = new StateDefinition("account", "disk");
|
|
||||||
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");
|
|
||||||
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
|
|
||||||
export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
|
|
||||||
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
|
|
||||||
export const ROUTER_DISK = new StateDefinition("router", "disk");
|
|
||||||
export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
|
export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
|
||||||
web: "disk-local",
|
web: "disk-local",
|
||||||
});
|
});
|
||||||
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
||||||
export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", {
|
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
|
||||||
web: "disk-local",
|
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");
|
||||||
});
|
export const PIN_DISK = new StateDefinition("pinUnlock", "disk");
|
||||||
|
export const PIN_MEMORY = new StateDefinition("pinUnlock", "memory");
|
||||||
|
export const ROUTER_DISK = new StateDefinition("router", "disk");
|
||||||
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
||||||
export const TOKEN_DISK = new StateDefinition("token", "disk");
|
export const TOKEN_DISK = new StateDefinition("token", "disk");
|
||||||
export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
|
export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
|
||||||
web: "disk-local",
|
web: "disk-local",
|
||||||
});
|
});
|
||||||
export const TOKEN_MEMORY = new StateDefinition("token", "memory");
|
export const TOKEN_MEMORY = new StateDefinition("token", "memory");
|
||||||
export const DEVICE_TRUST_DISK_LOCAL = new StateDefinition("deviceTrust", "disk", {
|
export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
|
||||||
web: "disk-local",
|
|
||||||
});
|
|
||||||
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
|
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
|
||||||
|
|
||||||
// Autofill
|
// Autofill
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue