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": {
|
||||
"message": "Account security"
|
||||
},
|
||||
"notifications": {
|
||||
"message": "Notifications"
|
||||
},
|
||||
"appearance": {
|
||||
"message": "Appearance"
|
||||
},
|
||||
"errorAssigningTargetCollection": {
|
||||
"message": "Error assigning target collection."
|
||||
},
|
||||
|
|
|
@ -4,20 +4,35 @@ import {
|
|||
} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
|
||||
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
|
||||
|
||||
import {
|
||||
encryptServiceFactory,
|
||||
EncryptServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/encrypt-service.factory";
|
||||
import {
|
||||
CachedServices,
|
||||
factory,
|
||||
FactoryOptions,
|
||||
} from "../../../platform/background/service-factories/factory-options";
|
||||
import {
|
||||
keyGenerationServiceFactory,
|
||||
KeyGenerationServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/key-generation-service.factory";
|
||||
import {
|
||||
stateProviderFactory,
|
||||
StateProviderInitOptions,
|
||||
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||
import {
|
||||
stateServiceFactory,
|
||||
StateServiceInitOptions,
|
||||
} from "../../../platform/background/service-factories/state-service.factory";
|
||||
|
||||
type MasterPasswordServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions &
|
||||
StateProviderInitOptions;
|
||||
StateProviderInitOptions &
|
||||
StateServiceInitOptions &
|
||||
KeyGenerationServiceInitOptions &
|
||||
EncryptServiceInitOptions;
|
||||
|
||||
export function internalMasterPasswordServiceFactory(
|
||||
cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices,
|
||||
|
@ -27,7 +42,13 @@ export function internalMasterPasswordServiceFactory(
|
|||
cache,
|
||||
"masterPasswordService",
|
||||
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,
|
||||
MasterPasswordServiceInitOptions,
|
||||
} from "./master-password-service.factory";
|
||||
import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory";
|
||||
import { PinServiceInitOptions, pinServiceFactory } from "./pin-service.factory";
|
||||
import {
|
||||
userDecryptionOptionsServiceFactory,
|
||||
UserDecryptionOptionsServiceInitOptions,
|
||||
|
@ -57,7 +57,7 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
|
|||
I18nServiceInitOptions &
|
||||
UserVerificationApiServiceInitOptions &
|
||||
UserDecryptionOptionsServiceInitOptions &
|
||||
PinCryptoServiceInitOptions &
|
||||
PinServiceInitOptions &
|
||||
LogServiceInitOptions &
|
||||
VaultTimeoutSettingsServiceInitOptions &
|
||||
PlatformUtilsServiceInitOptions &
|
||||
|
@ -80,7 +80,7 @@ export function userVerificationServiceFactory(
|
|||
await i18nServiceFactory(cache, opts),
|
||||
await userVerificationApiServiceFactory(cache, opts),
|
||||
await userDecryptionOptionsServiceFactory(cache, opts),
|
||||
await pinCryptoServiceFactory(cache, opts),
|
||||
await pinServiceFactory(cache, opts),
|
||||
await logServiceFactory(cache, opts),
|
||||
await vaultTimeoutSettingsServiceFactory(cache, opts),
|
||||
await platformUtilsServiceFactory(cache, opts),
|
||||
|
|
|
@ -12,8 +12,16 @@
|
|||
<input class="tw-font-mono" bitInput type="password" formControlName="pin" />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
<label class="tw-flex tw-items-start tw-gap-2" *ngIf="showMasterPassOnRestart">
|
||||
<input class="tw-mt-1" type="checkbox" bitCheckbox formControlName="masterPassOnRestart" />
|
||||
<label
|
||||
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>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Router } from "@angular/router";
|
|||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
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 { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
|
@ -63,7 +63,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
dialogService: DialogService,
|
||||
deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
userVerificationService: UserVerificationService,
|
||||
pinCryptoService: PinCryptoServiceAbstraction,
|
||||
pinService: PinServiceAbstraction,
|
||||
private routerService: BrowserRouterService,
|
||||
biometricStateService: BiometricStateService,
|
||||
accountService: AccountService,
|
||||
|
@ -89,7 +89,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
dialogService,
|
||||
deviceTrustService,
|
||||
userVerificationService,
|
||||
pinCryptoService,
|
||||
pinService,
|
||||
biometricStateService,
|
||||
accountService,
|
||||
authService,
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
} from "rxjs";
|
||||
|
||||
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 { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
|
@ -71,6 +72,7 @@ export class AccountSecurityComponent implements OnInit {
|
|||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private pinService: PinServiceAbstraction,
|
||||
private policyService: PolicyService,
|
||||
private formBuilder: FormBuilder,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
|
@ -131,7 +133,6 @@ export class AccountSecurityComponent implements OnInit {
|
|||
if (timeout === -2 && !showOnLocked) {
|
||||
timeout = -1;
|
||||
}
|
||||
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
|
||||
|
||||
this.form.controls.vaultTimeout.valueChanges
|
||||
.pipe(
|
||||
|
@ -153,12 +154,14 @@ export class AccountSecurityComponent implements OnInit {
|
|||
)
|
||||
.subscribe();
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
const initialValues = {
|
||||
vaultTimeout: timeout,
|
||||
vaultTimeoutAction: await firstValueFrom(
|
||||
this.vaultTimeoutSettingsService.vaultTimeoutAction$(),
|
||||
),
|
||||
pin: pinStatus !== "DISABLED",
|
||||
pin: await this.pinService.isPinSet(userId),
|
||||
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
||||
enableAutoBiometricsPrompt: await firstValueFrom(
|
||||
this.biometricStateService.promptAutomatically$,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<form #form (ngSubmit)="submit()">
|
||||
<header>
|
||||
<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>{{ "back" | i18n }}</span>
|
||||
</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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
|
||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||
import { enableAccountSwitching } from "../../platform/flags";
|
||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||
import { enableAccountSwitching } from "../../../platform/flags";
|
||||
|
||||
interface ExcludedDomain {
|
||||
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 {
|
||||
PinCryptoServiceAbstraction,
|
||||
PinCryptoService,
|
||||
PinServiceAbstraction,
|
||||
PinService,
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
AuthRequestServiceAbstraction,
|
||||
|
@ -319,7 +319,7 @@ export default class MainBackground {
|
|||
authRequestService: AuthRequestServiceAbstraction;
|
||||
accountService: AccountServiceAbstraction;
|
||||
globalStateProvider: GlobalStateProvider;
|
||||
pinCryptoService: PinCryptoServiceAbstraction;
|
||||
pinService: PinServiceAbstraction;
|
||||
singleUserStateProvider: SingleUserStateProvider;
|
||||
activeUserStateProvider: ActiveUserStateProvider;
|
||||
derivedStateProvider: DerivedStateProvider;
|
||||
|
@ -544,13 +544,31 @@ export default class MainBackground {
|
|||
|
||||
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.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.pinService,
|
||||
this.masterPasswordService,
|
||||
this.keyGenerationService,
|
||||
this.cryptoFunctionService,
|
||||
|
@ -705,6 +723,8 @@ export default class MainBackground {
|
|||
this.folderApiService = new FolderApiService(this.folderService, this.apiService);
|
||||
|
||||
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
||||
this.accountService,
|
||||
this.pinService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.cryptoService,
|
||||
this.tokenService,
|
||||
|
@ -713,14 +733,6 @@ export default class MainBackground {
|
|||
this.biometricStateService,
|
||||
);
|
||||
|
||||
this.pinCryptoService = new PinCryptoService(
|
||||
this.stateService,
|
||||
this.cryptoService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.logService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.userVerificationService = new UserVerificationService(
|
||||
this.stateService,
|
||||
this.cryptoService,
|
||||
|
@ -729,7 +741,7 @@ export default class MainBackground {
|
|||
this.i18nService,
|
||||
this.userVerificationApiService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.pinCryptoService,
|
||||
this.pinService,
|
||||
this.logService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.platformUtilsService,
|
||||
|
@ -851,11 +863,13 @@ export default class MainBackground {
|
|||
this.i18nService,
|
||||
this.collectionService,
|
||||
this.cryptoService,
|
||||
this.pinService,
|
||||
);
|
||||
|
||||
this.individualVaultExportService = new IndividualVaultExportService(
|
||||
this.folderService,
|
||||
this.cipherService,
|
||||
this.pinService,
|
||||
this.cryptoService,
|
||||
this.cryptoFunctionService,
|
||||
this.kdfConfigService,
|
||||
|
@ -864,6 +878,7 @@ export default class MainBackground {
|
|||
this.organizationVaultExportService = new OrganizationVaultExportService(
|
||||
this.cipherService,
|
||||
this.apiService,
|
||||
this.pinService,
|
||||
this.cryptoService,
|
||||
this.cryptoFunctionService,
|
||||
this.collectionService,
|
||||
|
@ -914,6 +929,7 @@ export default class MainBackground {
|
|||
};
|
||||
|
||||
this.systemService = new SystemService(
|
||||
this.pinService,
|
||||
this.messagingService,
|
||||
this.platformUtilsService,
|
||||
systemUtilsServiceReloadCallback,
|
||||
|
|
|
@ -356,7 +356,7 @@ export class NativeMessagingBackground {
|
|||
const masterKey = new SymmetricCryptoKey(
|
||||
Utils.fromB64ToArray(message.keyB64),
|
||||
) as MasterKey;
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
|
||||
masterKey,
|
||||
encUserKey,
|
||||
);
|
||||
|
|
|
@ -5,6 +5,14 @@ import {
|
|||
policyServiceFactory,
|
||||
PolicyServiceInitOptions,
|
||||
} 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 {
|
||||
tokenServiceFactory,
|
||||
TokenServiceInitOptions,
|
||||
|
@ -34,6 +42,8 @@ import {
|
|||
type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions &
|
||||
AccountServiceInitOptions &
|
||||
PinServiceInitOptions &
|
||||
UserDecryptionOptionsServiceInitOptions &
|
||||
CryptoServiceInitOptions &
|
||||
TokenServiceInitOptions &
|
||||
|
@ -51,6 +61,8 @@ export function vaultTimeoutSettingsServiceFactory(
|
|||
opts,
|
||||
async () =>
|
||||
new VaultTimeoutSettingsService(
|
||||
await accountServiceFactory(cache, opts),
|
||||
await pinServiceFactory(cache, opts),
|
||||
await userDecryptionOptionsServiceFactory(cache, opts),
|
||||
await cryptoServiceFactory(cache, opts),
|
||||
await tokenServiceFactory(cache, opts),
|
||||
|
|
|
@ -12,6 +12,10 @@ import {
|
|||
internalMasterPasswordServiceFactory,
|
||||
MasterPasswordServiceInitOptions,
|
||||
} from "../../../auth/background/service-factories/master-password-service.factory";
|
||||
import {
|
||||
PinServiceInitOptions,
|
||||
pinServiceFactory,
|
||||
} from "../../../auth/background/service-factories/pin-service.factory";
|
||||
import {
|
||||
StateServiceInitOptions,
|
||||
stateServiceFactory,
|
||||
|
@ -45,6 +49,7 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider
|
|||
type CryptoServiceFactoryOptions = FactoryOptions;
|
||||
|
||||
export type CryptoServiceInitOptions = CryptoServiceFactoryOptions &
|
||||
PinServiceInitOptions &
|
||||
MasterPasswordServiceInitOptions &
|
||||
KeyGenerationServiceInitOptions &
|
||||
CryptoFunctionServiceInitOptions &
|
||||
|
@ -67,6 +72,7 @@ export function cryptoServiceFactory(
|
|||
opts,
|
||||
async () =>
|
||||
new BrowserCryptoService(
|
||||
await pinServiceFactory(cache, opts),
|
||||
await internalMasterPasswordServiceFactory(cache, opts),
|
||||
await keyGenerationServiceFactory(cache, opts),
|
||||
await cryptoFunctionServiceFactory(cache, opts),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
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 {
|
||||
constructor(
|
||||
pinService: PinServiceAbstraction,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
keyGenerationService: KeyGenerationService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
|
@ -32,6 +34,7 @@ export class BrowserCryptoService extends CryptoService {
|
|||
kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
pinService,
|
||||
masterPasswordService,
|
||||
keyGenerationService,
|
||||
cryptoFunctionService,
|
||||
|
|
|
@ -163,6 +163,10 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
|
|||
* the view is open.
|
||||
*/
|
||||
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"));
|
||||
}
|
||||
|
||||
|
|
|
@ -196,12 +196,13 @@ export const routerTransition = trigger("routerTransition", [
|
|||
transition("vault-settings => sync", inSlideLeft),
|
||||
transition("sync => vault-settings", outSlideRight),
|
||||
|
||||
transition("tabs => excluded-domains", inSlideLeft),
|
||||
transition("excluded-domains => tabs", outSlideRight),
|
||||
|
||||
transition("tabs => options", inSlideLeft),
|
||||
transition("options => tabs", outSlideRight),
|
||||
|
||||
// Appearance settings
|
||||
transition("tabs => appearance", inSlideLeft),
|
||||
transition("appearance => tabs", outSlideRight),
|
||||
|
||||
transition("tabs => premium", inSlideLeft),
|
||||
transition("premium => tabs", outSlideRight),
|
||||
|
||||
|
@ -219,6 +220,13 @@ export const routerTransition = trigger("routerTransition", [
|
|||
transition("tabs => edit-send, send-type => edit-send", inSlideUp),
|
||||
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("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 { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.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 BrowserPopupUtils from "../platform/popup/browser-popup-utils";
|
||||
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 { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.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 { AppearanceComponent } from "../vault/popup/settings/appearance.component";
|
||||
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
|
||||
import { FoldersComponent } from "../vault/popup/settings/folders.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 { debounceNavigationGuard } from "./services/debounce-navigation.service";
|
||||
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
||||
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
||||
import { OptionsComponent } from "./settings/options.component";
|
||||
import { TabsV2Component } from "./tabs-v2.component";
|
||||
|
@ -254,6 +257,12 @@ const routes: Routes = [
|
|||
canActivate: [AuthGuard],
|
||||
data: { state: "account-security" },
|
||||
},
|
||||
{
|
||||
path: "notifications",
|
||||
component: NotifcationsSettingsComponent,
|
||||
canActivate: [AuthGuard],
|
||||
data: { state: "notifications" },
|
||||
},
|
||||
{
|
||||
path: "vault-settings",
|
||||
component: VaultSettingsComponent,
|
||||
|
@ -302,6 +311,12 @@ const routes: Routes = [
|
|||
canActivate: [AuthGuard],
|
||||
data: { state: "options" },
|
||||
},
|
||||
{
|
||||
path: "appearance",
|
||||
component: AppearanceComponent,
|
||||
canActivate: [AuthGuard],
|
||||
data: { state: "appearance" },
|
||||
},
|
||||
{
|
||||
path: "clone-cipher",
|
||||
component: AddEditComponent,
|
||||
|
@ -355,12 +370,11 @@ const routes: Routes = [
|
|||
data: { state: "tabs_current" },
|
||||
runGuardsAndResolvers: "always",
|
||||
},
|
||||
{
|
||||
...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, {
|
||||
path: "vault",
|
||||
component: VaultFilterComponent,
|
||||
canActivate: [AuthGuard],
|
||||
data: { state: "tabs_vault" },
|
||||
},
|
||||
}),
|
||||
{
|
||||
path: "generator",
|
||||
component: GeneratorComponent,
|
||||
|
|
|
@ -37,6 +37,8 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp
|
|||
import { TwoFactorComponent } from "../auth/popup/two-factor.component";
|
||||
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.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 { HeaderComponent } from "../platform/popup/header.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 { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.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 { 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 { FoldersComponent } from "../vault/popup/settings/folders.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 { UserVerificationComponent } from "./components/user-verification.component";
|
||||
import { ServicesModule } from "./services/services.module";
|
||||
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
|
||||
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
|
||||
import { OptionsComponent } from "./settings/options.component";
|
||||
import { TabsV2Component } from "./tabs-v2.component";
|
||||
|
@ -147,6 +150,8 @@ import "../platform/popup/locales";
|
|||
LoginViaAuthRequestComponent,
|
||||
LoginDecryptionOptionsComponent,
|
||||
OptionsComponent,
|
||||
NotifcationsSettingsComponent,
|
||||
AppearanceComponent,
|
||||
GeneratorComponent,
|
||||
PasswordGeneratorHistoryComponent,
|
||||
PasswordHistoryComponent,
|
||||
|
@ -181,6 +186,7 @@ import "../platform/popup/locales";
|
|||
EnvironmentSelectorComponent,
|
||||
CurrentAccountComponent,
|
||||
AccountSwitcherComponent,
|
||||
VaultV2Component,
|
||||
],
|
||||
providers: [CurrencyPipe, DatePipe],
|
||||
bootstrap: [AppComponent],
|
||||
|
|
|
@ -424,6 +424,10 @@ img,
|
|||
.modal-title,
|
||||
.overlay-container {
|
||||
user-select: none;
|
||||
|
||||
&.user-select {
|
||||
user-select: auto;
|
||||
}
|
||||
}
|
||||
|
||||
app-about .modal-body > *,
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
CLIENT_TYPE,
|
||||
} from "@bitwarden/angular/services/injection-tokens";
|
||||
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 { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
|
||||
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
|
@ -209,6 +209,7 @@ const safeProviders: SafeProvider[] = [
|
|||
safeProvider({
|
||||
provide: CryptoService,
|
||||
useFactory: (
|
||||
pinService: PinServiceAbstraction,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
keyGenerationService: KeyGenerationService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
|
@ -222,6 +223,7 @@ const safeProviders: SafeProvider[] = [
|
|||
kdfConfigService: KdfConfigService,
|
||||
) => {
|
||||
const cryptoService = new BrowserCryptoService(
|
||||
pinService,
|
||||
masterPasswordService,
|
||||
keyGenerationService,
|
||||
cryptoFunctionService,
|
||||
|
@ -238,6 +240,7 @@ const safeProviders: SafeProvider[] = [
|
|||
return cryptoService;
|
||||
},
|
||||
deps: [
|
||||
PinServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
KeyGenerationService,
|
||||
CryptoFunctionService,
|
||||
|
|
|
@ -60,67 +60,6 @@
|
|||
</div>
|
||||
<div id="totpHelp" class="box-footer">{{ "disableAutoTotpCopyDesc" | 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">
|
||||
<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>
|
||||
|
@ -192,56 +131,5 @@
|
|||
{{ "showIdentitiesCurrentTabDesc" | 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>
|
||||
<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>
|
||||
</main>
|
||||
|
|
|
@ -2,9 +2,7 @@ import { Component, OnInit } from "@angular/core";
|
|||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
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 { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
|
||||
import {
|
||||
UriMatchStrategy,
|
||||
|
@ -12,8 +10,6 @@ import {
|
|||
} from "@bitwarden/common/models/domain/domain-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 { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||
|
||||
import { enableAccountSwitching } from "../../platform/flags";
|
||||
|
@ -23,47 +19,29 @@ import { enableAccountSwitching } from "../../platform/flags";
|
|||
templateUrl: "options.component.html",
|
||||
})
|
||||
export class OptionsComponent implements OnInit {
|
||||
enableFavicon = false;
|
||||
enableBadgeCounter = true;
|
||||
enableAutoFillOnPageLoad = false;
|
||||
autoFillOnPageLoadDefault = false;
|
||||
autoFillOnPageLoadOptions: any[];
|
||||
enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true?
|
||||
enableContextMenuItem = false;
|
||||
enableAddLoginNotification = false;
|
||||
enableChangedPasswordNotification = false;
|
||||
enablePasskeys = true;
|
||||
showCardsCurrentTab = false;
|
||||
showIdentitiesCurrentTab = false;
|
||||
showClearClipboard = true;
|
||||
theme: ThemeType;
|
||||
themeOptions: any[];
|
||||
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
|
||||
uriMatchOptions: any[];
|
||||
clearClipboard: ClearClipboardDelaySetting;
|
||||
clearClipboardOptions: any[];
|
||||
showGeneral = true;
|
||||
showAutofill = true;
|
||||
showDisplay = true;
|
||||
accountSwitcherEnabled = false;
|
||||
|
||||
constructor(
|
||||
private messagingService: MessagingService,
|
||||
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||
private domainSettingsService: DomainSettingsService,
|
||||
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
||||
i18nService: I18nService,
|
||||
private themeStateService: ThemeStateService,
|
||||
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 = [
|
||||
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
||||
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
||||
|
@ -98,14 +76,6 @@ export class OptionsComponent implements OnInit {
|
|||
this.autofillSettingsService.autofillOnPageLoadDefault$,
|
||||
);
|
||||
|
||||
this.enableAddLoginNotification = await firstValueFrom(
|
||||
this.userNotificationSettingsService.enableAddedLoginPrompt$,
|
||||
);
|
||||
|
||||
this.enableChangedPasswordNotification = await firstValueFrom(
|
||||
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
|
||||
);
|
||||
|
||||
this.enableContextMenuItem = await firstValueFrom(
|
||||
this.autofillSettingsService.enableContextMenu$,
|
||||
);
|
||||
|
@ -117,14 +87,6 @@ export class OptionsComponent implements OnInit {
|
|||
|
||||
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(
|
||||
this.domainSettingsService.defaultUriMatchStrategy$,
|
||||
);
|
||||
|
@ -133,22 +95,6 @@ export class OptionsComponent implements OnInit {
|
|||
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() {
|
||||
await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem);
|
||||
this.messagingService.send("bgUpdateContextMenu");
|
||||
|
@ -166,15 +112,6 @@ export class OptionsComponent implements OnInit {
|
|||
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() {
|
||||
await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab);
|
||||
}
|
||||
|
@ -183,10 +120,6 @@ export class OptionsComponent implements OnInit {
|
|||
await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab);
|
||||
}
|
||||
|
||||
async saveTheme() {
|
||||
await this.themeStateService.setSelectedTheme(this.theme);
|
||||
}
|
||||
|
||||
async saveClearClipboard() {
|
||||
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { ImportService, ImportServiceAbstraction } from "@bitwarden/importer/core";
|
||||
|
||||
import {
|
||||
pinServiceFactory,
|
||||
PinServiceInitOptions,
|
||||
} from "../../../auth/background/service-factories/pin-service.factory";
|
||||
import {
|
||||
cryptoServiceFactory,
|
||||
CryptoServiceInitOptions,
|
||||
|
@ -36,7 +40,8 @@ export type ImportServiceInitOptions = ImportServiceFactoryOptions &
|
|||
ImportApiServiceInitOptions &
|
||||
I18nServiceInitOptions &
|
||||
CollectionServiceInitOptions &
|
||||
CryptoServiceInitOptions;
|
||||
CryptoServiceInitOptions &
|
||||
PinServiceInitOptions;
|
||||
|
||||
export function importServiceFactory(
|
||||
cache: {
|
||||
|
@ -56,6 +61,7 @@ export function importServiceFactory(
|
|||
await i18nServiceFactory(cache, opts),
|
||||
await collectionServiceFactory(cache, opts),
|
||||
await cryptoServiceFactory(cache, opts),
|
||||
await pinServiceFactory(cache, opts),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div bitDialogTitle>Bitwarden</div>
|
||||
<div bitDialogContent>
|
||||
<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">
|
||||
<p *ngIf="data.isCloud">
|
||||
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
|
||||
|
@ -16,7 +16,7 @@
|
|||
|
||||
<ng-container *ngIf="!data.isCloud">
|
||||
<ng-container *ngIf="data.serverConfig.server">
|
||||
<p>
|
||||
<p class="user-select">
|
||||
{{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>:
|
||||
{{ data.serverConfig?.version }}
|
||||
<span *ngIf="!data.serverConfig.isValid()">
|
||||
|
@ -28,7 +28,7 @@
|
|||
</div>
|
||||
</ng-container>
|
||||
|
||||
<p *ngIf="!data.serverConfig.server">
|
||||
<p class="user-select" *ngIf="!data.serverConfig.server">
|
||||
{{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>:
|
||||
{{ data.serverConfig?.version }}
|
||||
<span *ngIf="!data.serverConfig.isValid()">
|
|
@ -9,11 +9,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||
import { ButtonModule, DialogModule } from "@bitwarden/components";
|
||||
|
||||
@Component({
|
||||
templateUrl: "about.component.html",
|
||||
templateUrl: "about-dialog.component.html",
|
||||
standalone: true,
|
||||
imports: [CommonModule, JslibModule, DialogModule, ButtonModule],
|
||||
})
|
||||
export class AboutComponent {
|
||||
export class AboutDialogComponent {
|
||||
protected year = new Date().getFullYear();
|
||||
protected version$: Observable<string>;
|
||||
|
|
@ -30,17 +30,17 @@
|
|||
<button
|
||||
type="button"
|
||||
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>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -86,6 +86,14 @@
|
|||
<div class="row-main">{{ "options" | i18n }}</div>
|
||||
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
|
||||
</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
|
||||
type="button"
|
||||
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 BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
|
||||
|
||||
import { AboutComponent } from "./about/about.component";
|
||||
import { AboutDialogComponent } from "./about-dialog/about-dialog.component";
|
||||
|
||||
const RateUrls = {
|
||||
[DeviceType.ChromeExtension]:
|
||||
|
@ -84,7 +84,7 @@ export class SettingsComponent implements OnInit {
|
|||
}
|
||||
|
||||
about() {
|
||||
this.dialogService.open(AboutComponent);
|
||||
this.dialogService.open(AboutDialogComponent);
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
|
||||
if (await this.keyConnectorService.getConvertAccountRequired()) {
|
||||
|
|
|
@ -10,8 +10,8 @@ import {
|
|||
AuthRequestService,
|
||||
LoginStrategyService,
|
||||
LoginStrategyServiceAbstraction,
|
||||
PinCryptoService,
|
||||
PinCryptoServiceAbstraction,
|
||||
PinService,
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||
|
@ -208,7 +208,7 @@ export class Main {
|
|||
cipherFileUploadService: CipherFileUploadService;
|
||||
keyConnectorService: KeyConnectorService;
|
||||
userVerificationService: UserVerificationService;
|
||||
pinCryptoService: PinCryptoServiceAbstraction;
|
||||
pinService: PinServiceAbstraction;
|
||||
stateService: StateService;
|
||||
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
||||
domainSettingsService: DomainSettingsService;
|
||||
|
@ -363,11 +363,29 @@ export class Main {
|
|||
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.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.pinService,
|
||||
this.masterPasswordService,
|
||||
this.keyGenerationService,
|
||||
this.cryptoFunctionService,
|
||||
|
@ -582,6 +600,8 @@ export class Main {
|
|||
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
|
||||
|
||||
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
||||
this.accountService,
|
||||
this.pinService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.cryptoService,
|
||||
this.tokenService,
|
||||
|
@ -590,14 +610,6 @@ export class Main {
|
|||
this.biometricStateService,
|
||||
);
|
||||
|
||||
this.pinCryptoService = new PinCryptoService(
|
||||
this.stateService,
|
||||
this.cryptoService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.logService,
|
||||
this.kdfConfigService,
|
||||
);
|
||||
|
||||
this.userVerificationService = new UserVerificationService(
|
||||
this.stateService,
|
||||
this.cryptoService,
|
||||
|
@ -606,7 +618,7 @@ export class Main {
|
|||
this.i18nService,
|
||||
this.userVerificationApiService,
|
||||
this.userDecryptionOptionsService,
|
||||
this.pinCryptoService,
|
||||
this.pinService,
|
||||
this.logService,
|
||||
this.vaultTimeoutSettingsService,
|
||||
this.platformUtilsService,
|
||||
|
@ -669,11 +681,13 @@ export class Main {
|
|||
this.i18nService,
|
||||
this.collectionService,
|
||||
this.cryptoService,
|
||||
this.pinService,
|
||||
);
|
||||
|
||||
this.individualExportService = new IndividualVaultExportService(
|
||||
this.folderService,
|
||||
this.cipherService,
|
||||
this.pinService,
|
||||
this.cryptoService,
|
||||
this.cryptoFunctionService,
|
||||
this.kdfConfigService,
|
||||
|
@ -682,6 +696,7 @@ export class Main {
|
|||
this.organizationExportService = new OrganizationVaultExportService(
|
||||
this.cipherService,
|
||||
this.apiService,
|
||||
this.pinService,
|
||||
this.cryptoService,
|
||||
this.cryptoFunctionService,
|
||||
this.collectionService,
|
||||
|
@ -693,6 +708,8 @@ export class Main {
|
|||
this.organizationExportService,
|
||||
);
|
||||
|
||||
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
|
||||
|
||||
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
|
||||
this.program = new Program(this);
|
||||
this.vaultProgram = new VaultProgram(this);
|
||||
|
@ -716,8 +733,6 @@ export class Main {
|
|||
);
|
||||
|
||||
this.providerApiService = new ProviderApiService(this.apiService);
|
||||
|
||||
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
|
||||
}
|
||||
|
||||
async run() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as chalk from "chalk";
|
||||
import { program, Command, OptionValues } from "commander";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
|
||||
|
@ -63,8 +64,16 @@ export class Program {
|
|||
process.env.BW_NOINTERACTION = "true";
|
||||
});
|
||||
|
||||
program.on("option:session", (key) => {
|
||||
program.on("option:session", async (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:*", () => {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import { Component, OnInit } from "@angular/core";
|
||||
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 { 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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
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 { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-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 { StateService } from "@bitwarden/common/platform/abstractions/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 { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
|
@ -111,6 +112,7 @@ export class SettingsComponent implements OnInit {
|
|||
private destroy$ = new Subject<void>();
|
||||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private policyService: PolicyService,
|
||||
private formBuilder: FormBuilder,
|
||||
private i18nService: I18nService,
|
||||
|
@ -127,6 +129,7 @@ export class SettingsComponent implements OnInit {
|
|||
private desktopSettingsService: DesktopSettingsService,
|
||||
private biometricStateService: BiometricStateService,
|
||||
private desktopAutofillSettingsService: DesktopAutofillSettingsService,
|
||||
private pinService: PinServiceAbstraction,
|
||||
private authRequestService: AuthRequestServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private nativeMessagingManifestService: NativeMessagingManifestService,
|
||||
|
@ -243,9 +246,10 @@ export class SettingsComponent implements OnInit {
|
|||
}),
|
||||
);
|
||||
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
// Load initial values
|
||||
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
|
||||
this.userHasPinSet = pinStatus !== "DISABLED";
|
||||
this.userHasPinSet = await this.pinService.isPinSet(userId);
|
||||
|
||||
const initialValues = {
|
||||
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 { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions";
|
||||
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
|
||||
import { Account } from "../../models/account";
|
||||
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
|
||||
|
@ -183,6 +184,7 @@ const safeProviders: SafeProvider[] = [
|
|||
provide: SystemServiceAbstraction,
|
||||
useClass: SystemService,
|
||||
deps: [
|
||||
PinServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
RELOAD_CALLBACK,
|
||||
|
@ -250,6 +252,7 @@ const safeProviders: SafeProvider[] = [
|
|||
provide: CryptoServiceAbstraction,
|
||||
useClass: ElectronCryptoService,
|
||||
deps: [
|
||||
PinServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
KeyGenerationServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
|
|
|
@ -12,8 +12,16 @@
|
|||
<input class="tw-font-mono" bitInput type="password" formControlName="pin" />
|
||||
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
|
||||
</bit-form-field>
|
||||
<label class="tw-flex tw-items-start tw-gap-2" *ngIf="showMasterPassOnRestart">
|
||||
<input class="tw-mt-1" type="checkbox" bitCheckbox formControlName="masterPassOnRestart" />
|
||||
<label
|
||||
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>
|
||||
</label>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { of } from "rxjs";
|
|||
|
||||
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
|
||||
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 { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
|
@ -155,8 +155,8 @@ describe("LockComponent", () => {
|
|||
useValue: mock<UserVerificationService>(),
|
||||
},
|
||||
{
|
||||
provide: PinCryptoServiceAbstraction,
|
||||
useValue: mock<PinCryptoServiceAbstraction>(),
|
||||
provide: PinServiceAbstraction,
|
||||
useValue: mock<PinServiceAbstraction>(),
|
||||
},
|
||||
{
|
||||
provide: BiometricStateService,
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router";
|
|||
import { firstValueFrom, switchMap } from "rxjs";
|
||||
|
||||
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 { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
|
||||
|
@ -62,7 +62,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
dialogService: DialogService,
|
||||
deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
userVerificationService: UserVerificationService,
|
||||
pinCryptoService: PinCryptoServiceAbstraction,
|
||||
pinService: PinServiceAbstraction,
|
||||
biometricStateService: BiometricStateService,
|
||||
accountService: AccountService,
|
||||
authService: AuthService,
|
||||
|
@ -88,7 +88,7 @@ export class LockComponent extends BaseLockComponent {
|
|||
dialogService,
|
||||
deviceTrustService,
|
||||
userVerificationService,
|
||||
pinCryptoService,
|
||||
pinService,
|
||||
biometricStateService,
|
||||
accountService,
|
||||
authService,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
|
||||
import { mock } from "jest-mock-extended";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
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";
|
||||
|
@ -26,6 +27,7 @@ import { ElectronCryptoService } from "./electron-crypto.service";
|
|||
describe("electronCryptoService", () => {
|
||||
let sut: ElectronCryptoService;
|
||||
|
||||
const pinService = mock<PinServiceAbstraction>();
|
||||
const keyGenerationService = mock<KeyGenerationService>();
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
|
@ -46,6 +48,7 @@ describe("electronCryptoService", () => {
|
|||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
sut = new ElectronCryptoService(
|
||||
pinService,
|
||||
masterPasswordService,
|
||||
keyGenerationService,
|
||||
cryptoFunctionService,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "@bitwarden/auth/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
|
||||
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 {
|
||||
constructor(
|
||||
pinService: PinServiceAbstraction,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
keyGenerationService: KeyGenerationService,
|
||||
cryptoFunctionService: CryptoFunctionService,
|
||||
|
@ -35,6 +37,7 @@ export class ElectronCryptoService extends CryptoService {
|
|||
kdfConfigService: KdfConfigService,
|
||||
) {
|
||||
super(
|
||||
pinService,
|
||||
masterPasswordService,
|
||||
keyGenerationService,
|
||||
cryptoFunctionService,
|
||||
|
@ -174,7 +177,10 @@ export class ElectronCryptoService extends CryptoService {
|
|||
if (!encUserKey) {
|
||||
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
|
||||
await this.storeBiometricKey(userKey, userId);
|
||||
await this.stateService.setCryptoMasterKeyBiometric(null, { userId });
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { concatMap, takeUntil, map, lastValueFrom } from "rxjs";
|
||||
import { concatMap, takeUntil, map } from "rxjs";
|
||||
import { tap } from "rxjs/operators";
|
||||
|
||||
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 { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
|
||||
import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-verify.component";
|
||||
|
||||
@Component({
|
||||
selector: "app-two-factor-setup",
|
||||
|
@ -66,17 +65,17 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
|
|||
async manage(type: TwoFactorProviderType) {
|
||||
switch (type) {
|
||||
case TwoFactorProviderType.OrganizationDuo: {
|
||||
const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, {
|
||||
data: { type: type, organizationId: this.organizationId },
|
||||
});
|
||||
const result: AuthResponse<TwoFactorDuoResponse> = await lastValueFrom(
|
||||
twoFactorVerifyDialogRef.closed,
|
||||
const result: AuthResponse<TwoFactorDuoResponse> = await this.callTwoFactorVerifyDialog(
|
||||
TwoFactorProviderType.OrganizationDuo,
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
|
||||
duoComp.type = TwoFactorProviderType.OrganizationDuo;
|
||||
duoComp.organizationId = this.organizationId;
|
||||
duoComp.auth(result);
|
||||
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
|
||||
this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo);
|
||||
|
|
|
@ -2,7 +2,17 @@ import { DOCUMENT } from "@angular/common";
|
|||
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||
import { NavigationEnd, Router } from "@angular/router";
|
||||
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 { 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 { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||
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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
@ -242,8 +253,12 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||
new SendOptionsPolicy(),
|
||||
]);
|
||||
|
||||
this.paymentMethodWarningsRefresh$
|
||||
combineLatest([
|
||||
this.configService.getFeatureFlag$(FeatureFlag.ShowPaymentMethodWarningBanners),
|
||||
this.paymentMethodWarningsRefresh$,
|
||||
])
|
||||
.pipe(
|
||||
filter(([showPaymentMethodWarningBanners]) => showPaymentMethodWarningBanners),
|
||||
switchMap(() => this.organizationService.memberOrganizations$),
|
||||
switchMap(
|
||||
async (organizations) =>
|
||||
|
|
|
@ -6,6 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
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 { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
|
@ -50,6 +51,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
|||
private userVerificationService: UserVerificationService,
|
||||
private keyRotationService: UserKeyRotationService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
|
@ -61,6 +63,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
|||
stateService,
|
||||
dialogService,
|
||||
kdfConfigService,
|
||||
masterPasswordService,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -165,7 +168,22 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
|
|||
newMasterKey: MasterKey,
|
||||
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();
|
||||
request.masterPasswordHash = await this.cryptoService.hashMasterKey(
|
||||
this.currentMasterPassword,
|
||||
|
|
|
@ -6,6 +6,7 @@ import { takeUntil } from "rxjs";
|
|||
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
|
@ -60,6 +61,7 @@ export class EmergencyAccessTakeoverComponent
|
|||
dialogService: DialogService,
|
||||
private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>,
|
||||
kdfConfigService: KdfConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
|
@ -71,6 +73,7 @@ export class EmergencyAccessTakeoverComponent
|
|||
stateService,
|
||||
dialogService,
|
||||
kdfConfigService,
|
||||
masterPasswordService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitward
|
|||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
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 { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
@ -34,6 +35,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
|
|||
userVerificationService: UserVerificationService,
|
||||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
router,
|
||||
|
@ -49,6 +51,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
|
|||
logService,
|
||||
dialogService,
|
||||
kdfConfigService,
|
||||
masterPasswordService,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,56 +1,56 @@
|
|||
<div *ngIf="selfHosted" class="page-header">
|
||||
<h1>{{ "subscription" | i18n }}</h1>
|
||||
</div>
|
||||
<div *ngIf="!selfHosted" class="tabbed-header">
|
||||
<h1>{{ "goPremium" | i18n }}</h1>
|
||||
</div>
|
||||
<bit-callout
|
||||
<bit-section>
|
||||
<h2 *ngIf="!selfHosted" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
|
||||
<bit-callout
|
||||
type="info"
|
||||
*ngIf="canAccessPremium$ | async"
|
||||
title="{{ 'youHavePremiumAccess' | i18n }}"
|
||||
icon="bwi bwi-star-f"
|
||||
>
|
||||
>
|
||||
{{ "alreadyPremiumFromOrg" | i18n }}
|
||||
</bit-callout>
|
||||
<bit-callout type="success">
|
||||
</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>
|
||||
<i class="bwi bwi-check tw-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>
|
||||
<i class="bwi bwi-check tw-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>
|
||||
<i class="bwi bwi-check tw-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>
|
||||
<i class="bwi bwi-check tw-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>
|
||||
<i class="bwi bwi-check tw-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>
|
||||
<i class="bwi bwi-check tw-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>
|
||||
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
|
||||
{{ "premiumSignUpFuture" | i18n }}
|
||||
</li>
|
||||
</ul>
|
||||
<p class="text-lg" [ngClass]="{ 'mb-0': !selfHosted }">
|
||||
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !selfHosted }">
|
||||
{{
|
||||
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
|
||||
}}
|
||||
<a routerLink="/create-organization" [queryParams]="{ plan: 'families' }">{{
|
||||
"bitwardenFamiliesPlan" | i18n
|
||||
}}</a>
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
routerLink="/create-organization"
|
||||
[queryParams]="{ plan: 'families' }"
|
||||
>{{ "bitwardenFamiliesPlan" | i18n }}</a
|
||||
>
|
||||
</p>
|
||||
<a
|
||||
bitButton
|
||||
|
@ -62,68 +62,82 @@
|
|||
>
|
||||
{{ "purchasePremium" | i18n }}
|
||||
</a>
|
||||
</bit-callout>
|
||||
<ng-container *ngIf="selfHosted">
|
||||
<p>{{ "uploadLicenseFilePremium" | i18n }}</p>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="form-group">
|
||||
<label for="file">{{ "licenseFile" | i18n }}</label>
|
||||
<input type="file" id="file" class="form-control-file" name="file" required />
|
||||
<small class="form-text text-muted">{{
|
||||
"licenseFileDesc" | i18n: "bitwarden_premium_license.json"
|
||||
}}</small>
|
||||
</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>
|
||||
<button type="submit" buttonType="primary" bitButton [loading]="form.loading">
|
||||
<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 }}
|
||||
</button>
|
||||
</form>
|
||||
</ng-container>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted">
|
||||
<h2 class="mt-5">{{ "addons" | i18n }}</h2>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="additionalStorage">{{ "additionalStorageGb" | i18n }}</label>
|
||||
</bit-section>
|
||||
<form [formGroup]="addonForm" [bitSubmit]="submit" *ngIf="!selfHosted">
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||
<input
|
||||
id="additionalStorage"
|
||||
class="form-control"
|
||||
bitInput
|
||||
formControlName="additionalStorage"
|
||||
type="number"
|
||||
name="AdditionalStorageGb"
|
||||
[(ngModel)]="additionalStorage"
|
||||
min="0"
|
||||
max="99"
|
||||
step="1"
|
||||
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||
/>
|
||||
<small class="text-muted form-text">{{
|
||||
<bit-hint>{{
|
||||
"additionalStorageIntervalDesc"
|
||||
| i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n)
|
||||
}}</small>
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{ "summary" | i18n }}</h2>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
|
||||
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
|
||||
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB ×
|
||||
{{ storageGbPrice | currency: "$" }} =
|
||||
{{ additionalStorageTotal | currency: "$" }}
|
||||
<hr class="my-3" />
|
||||
<h2 class="spaced-header mb-4">{{ "paymentInformation" | i18n }}</h2>
|
||||
<hr class="tw-my-3" />
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
|
||||
<app-payment [hideBank]="true"></app-payment>
|
||||
<app-tax-info></app-tax-info>
|
||||
<div id="price" class="my-4">
|
||||
<div class="text-muted text-sm">
|
||||
<div id="price" class="tw-my-4">
|
||||
<div class="tw-text-muted tw-text-sm">
|
||||
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}
|
||||
<br />
|
||||
<ng-container>
|
||||
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
|
||||
</ng-container>
|
||||
</div>
|
||||
<hr class="my-1 col-3 ml-0" />
|
||||
<p class="text-lg">
|
||||
<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>
|
||||
<small class="text-muted font-italic">{{ "paymentChargedAnnually" | i18n }}</small>
|
||||
<button type="submit" bitButton [loading]="form.loading">
|
||||
<p bitTypography="body2">{{ "paymentChargedAnnually" | i18n }}</p>
|
||||
<button type="submit" bitButton bitFormButton>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
</bit-section>
|
||||
</form>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Component, OnInit, ViewChild } from "@angular/core";
|
||||
import { FormControl, FormGroup, Validators } from "@angular/forms";
|
||||
import { Router } from "@angular/router";
|
||||
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 { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
|
@ -26,11 +26,16 @@ export class PremiumComponent implements OnInit {
|
|||
premiumPrice = 10;
|
||||
familyPlanMaxUserCount = 6;
|
||||
storageGbPrice = 4;
|
||||
additionalStorage = 0;
|
||||
cloudWebVaultUrl: string;
|
||||
licenseFile: File = null;
|
||||
|
||||
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(
|
||||
private apiService: ApiService,
|
||||
private i18nService: I18nService,
|
||||
|
@ -39,14 +44,17 @@ export class PremiumComponent implements OnInit {
|
|||
private router: Router,
|
||||
private messagingService: MessagingService,
|
||||
private syncService: SyncService,
|
||||
private logService: LogService,
|
||||
private environmentService: EnvironmentService,
|
||||
private billingAccountProfileStateService: BillingAccountProfileStateService,
|
||||
) {
|
||||
this.selfHosted = platformUtilsService.isSelfHost();
|
||||
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() {
|
||||
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
|
||||
if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) {
|
||||
|
@ -56,13 +64,11 @@ export class PremiumComponent implements OnInit {
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async submit() {
|
||||
let files: FileList = null;
|
||||
submit = async () => {
|
||||
this.licenseForm.markAllAsTouched();
|
||||
this.addonForm.markAllAsTouched();
|
||||
if (this.selfHosted) {
|
||||
const fileEl = document.getElementById("file") as HTMLInputElement;
|
||||
files = fileEl.files;
|
||||
if (files == null || files.length === 0) {
|
||||
if (this.licenseFile == null) {
|
||||
this.platformUtilsService.showToast(
|
||||
"error",
|
||||
this.i18nService.t("errorOccurred"),
|
||||
|
@ -72,7 +78,6 @@ export class PremiumComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.selfHosted) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
if (!this.tokenService.getEmailVerified()) {
|
||||
|
@ -85,12 +90,12 @@ export class PremiumComponent implements OnInit {
|
|||
}
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append("license", files[0]);
|
||||
this.formPromise = this.apiService.postAccountLicense(fd).then(() => {
|
||||
fd.append("license", this.licenseFile);
|
||||
await this.apiService.postAccountLicense(fd).then(() => {
|
||||
return this.finalizePremium();
|
||||
});
|
||||
} else {
|
||||
this.formPromise = this.paymentComponent
|
||||
await this.paymentComponent
|
||||
.createPaymentToken()
|
||||
.then((result) => {
|
||||
const fd = new FormData();
|
||||
|
@ -114,11 +119,7 @@ export class PremiumComponent implements OnInit {
|
|||
}
|
||||
});
|
||||
}
|
||||
await this.formPromise;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async finalizePremium() {
|
||||
await this.apiService.refreshIdentityToken();
|
||||
|
@ -127,6 +128,9 @@ export class PremiumComponent implements OnInit {
|
|||
await this.router.navigate(["/settings/subscription/user-subscription"]);
|
||||
}
|
||||
|
||||
get additionalStorage(): number {
|
||||
return this.addonForm.get("additionalStorage").value;
|
||||
}
|
||||
get additionalStorageTotal(): number {
|
||||
return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
|
||||
}
|
||||
|
|
|
@ -1,65 +1,57 @@
|
|||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="form-group col-8">
|
||||
<label for="newSeatCount">{{ "subscriptionSeats" | i18n }}</label>
|
||||
<input
|
||||
id="newSeatCount"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="NewSeatCount"
|
||||
[(ngModel)]="newSeatCount"
|
||||
min="0"
|
||||
step="1"
|
||||
required
|
||||
/>
|
||||
<small class="d-block text-muted mb-4">
|
||||
<form [formGroup]="adjustSubscriptionForm" [bitSubmit]="submit">
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<div class="tw-col-span-8">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "subscriptionSeats" | i18n }}</bit-label>
|
||||
<input bitInput formControlName="newSeatCount" type="number" min="0" step="1" />
|
||||
<bit-hint>
|
||||
<strong>{{ "total" | i18n }}:</strong> {{ additionalSeatCount || 0 }} ×
|
||||
{{ seatPrice | currency: "$" }} = {{ adjustedSeatTotal | currency: "$" }} /
|
||||
{{ interval | i18n }}
|
||||
</small>
|
||||
{{ interval | i18n }}</bit-hint
|
||||
>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4">
|
||||
<div class="form-group col-sm">
|
||||
<div class="form-check">
|
||||
<div>
|
||||
<bit-form-control>
|
||||
<input
|
||||
id="limitSubscription"
|
||||
class="form-check-input"
|
||||
bitCheckbox
|
||||
formControlName="limitSubscription"
|
||||
type="checkbox"
|
||||
name="LimitSubscription"
|
||||
[(ngModel)]="limitSubscription"
|
||||
(change)="limitSubscriptionChanged()"
|
||||
/>
|
||||
<label for="limitSubscription">{{ "limitSubscription" | i18n }}</label>
|
||||
<bit-label>{{ "limitSubscription" | i18n }}</bit-label>
|
||||
<bit-hint> {{ "limitSubscriptionDesc" | i18n }}</bit-hint>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
<small class="d-block text-muted">{{ "limitSubscriptionDesc" | i18n }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-4" [hidden]="!limitSubscription">
|
||||
<div class="form-group col-sm">
|
||||
<label for="maxAutoscaleSeats">{{ "maxSeatLimit" | i18n }}</label>
|
||||
<div
|
||||
class="tw-grid tw-grid-cols-12 tw-gap-4 tw-mb-4"
|
||||
[hidden]="!adjustSubscriptionForm.value.limitSubscription"
|
||||
>
|
||||
<div class="tw-col-span-8">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "maxSeatLimit" | i18n }}</bit-label>
|
||||
<input
|
||||
id="maxAutoscaleSeats"
|
||||
class="form-control col-8"
|
||||
bitInput
|
||||
formControlName="newMaxSeats"
|
||||
type="number"
|
||||
name="MaxAutoscaleSeats"
|
||||
[(ngModel)]="newMaxSeats"
|
||||
[min]="newSeatCount == null ? 1 : newSeatCount"
|
||||
[min]="
|
||||
adjustSubscriptionForm.value.newSeatCount == null
|
||||
? 1
|
||||
: adjustSubscriptionForm.value.newSeatCount
|
||||
"
|
||||
step="1"
|
||||
[required]="limitSubscription"
|
||||
/>
|
||||
<small class="d-block text-muted">
|
||||
<bit-hint>
|
||||
<strong>{{ "maxSeatCost" | i18n }}:</strong> {{ additionalMaxSeatCount || 0 }} ×
|
||||
{{ seatPrice | currency: "$" }} = {{ maxSeatTotal | currency: "$" }} /
|
||||
{{ interval | i18n }}
|
||||
</small>
|
||||
{{ interval | i18n }}</bit-hint
|
||||
>
|
||||
</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 bitButton buttonType="primary" bitFormButton type="submit">
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<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 { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request";
|
||||
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";
|
||||
|
||||
@Component({
|
||||
selector: "app-adjust-subscription",
|
||||
templateUrl: "adjust-subscription.component.html",
|
||||
})
|
||||
export class AdjustSubscription {
|
||||
export class AdjustSubscription implements OnInit, OnDestroy {
|
||||
@Input() organizationId: string;
|
||||
@Input() maxAutoscaleSeats: number;
|
||||
@Input() currentSeatCount: number;
|
||||
@Input() seatPrice = 0;
|
||||
@Input() interval = "year";
|
||||
@Output() onAdjusted = new EventEmitter();
|
||||
private destroy$ = new Subject<void>();
|
||||
|
||||
formPromise: Promise<void>;
|
||||
limitSubscription: boolean;
|
||||
newSeatCount: number;
|
||||
newMaxSeats: number;
|
||||
|
||||
adjustSubscriptionForm = this.formBuilder.group({
|
||||
newSeatCount: [0, [Validators.min(0)]],
|
||||
limitSubscription: [false],
|
||||
newMaxSeats: [0, [Validators.min(0)]],
|
||||
});
|
||||
get limitSubscription(): boolean {
|
||||
return this.adjustSubscriptionForm.value.limitSubscription;
|
||||
}
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private logService: LogService,
|
||||
private organizationApiService: OrganizationApiServiceAbstraction,
|
||||
private formBuilder: FormBuilder,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.limitSubscription = this.maxAutoscaleSeats != null;
|
||||
this.newSeatCount = this.currentSeatCount;
|
||||
this.newMaxSeats = this.maxAutoscaleSeats;
|
||||
this.adjustSubscriptionForm.patchValue({
|
||||
newSeatCount: this.currentSeatCount,
|
||||
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() {
|
||||
try {
|
||||
ngOnDestroy() {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
submit = async () => {
|
||||
this.adjustSubscriptionForm.markAllAsTouched();
|
||||
if (this.adjustSubscriptionForm.invalid) {
|
||||
return;
|
||||
}
|
||||
const request = new OrganizationSubscriptionUpdateRequest(
|
||||
this.additionalSeatCount,
|
||||
this.newMaxSeats,
|
||||
);
|
||||
this.formPromise = this.organizationApiService.updatePasswordManagerSeats(
|
||||
this.organizationId,
|
||||
request,
|
||||
this.adjustSubscriptionForm.value.newMaxSeats,
|
||||
);
|
||||
await this.organizationApiService.updatePasswordManagerSeats(this.organizationId, request);
|
||||
|
||||
await this.formPromise;
|
||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated"));
|
||||
|
||||
this.platformUtilsService.showToast(
|
||||
"success",
|
||||
null,
|
||||
this.i18nService.t("subscriptionUpdated"),
|
||||
);
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
this.onAdjusted.emit();
|
||||
}
|
||||
};
|
||||
|
||||
limitSubscriptionChanged() {
|
||||
if (!this.limitSubscription) {
|
||||
this.newMaxSeats = null;
|
||||
if (!this.adjustSubscriptionForm.value.limitSubscription) {
|
||||
this.adjustSubscriptionForm.value.newMaxSeats = null;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
return this.newMaxSeats ? this.newMaxSeats - this.currentSeatCount : 0;
|
||||
return this.adjustSubscriptionForm.value.newMaxSeats
|
||||
? this.adjustSubscriptionForm.value.newMaxSeats - this.currentSeatCount
|
||||
: 0;
|
||||
}
|
||||
|
||||
get adjustedSeatTotal(): number {
|
||||
|
|
|
@ -1,142 +1,153 @@
|
|||
<ng-container *ngIf="loading">
|
||||
<i
|
||||
class="bwi bwi-spinner bwi-spin text-muted"
|
||||
class="bwi bwi-spinner bwi-spin tw-text-muted"
|
||||
title="{{ 'loading' | i18n }}"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||
<span class="tw-sr-only">{{ "loading" | i18n }}</span>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="createOrganization && selfHosted">
|
||||
<p>{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate>
|
||||
<div class="form-group">
|
||||
<label for="file">{{ "licenseFile" | i18n }}</label>
|
||||
<input type="file" id="file" class="form-control-file" name="file" required />
|
||||
<small class="form-text text-muted">{{
|
||||
"licenseFileDesc" | i18n: "bitwarden_organization_license.json"
|
||||
}}</small>
|
||||
<p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
|
||||
<form [formGroup]="selfHostedForm" [bitSubmit]="submit">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
|
||||
<div>
|
||||
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
|
||||
{{ "chooseFile" | i18n }}
|
||||
</button>
|
||||
{{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
|
||||
</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>{{ "submit" | i18n }}</span>
|
||||
<input
|
||||
#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>
|
||||
</form>
|
||||
</ng-container>
|
||||
<form
|
||||
#form
|
||||
[formGroup]="formGroup"
|
||||
(ngSubmit)="submit()"
|
||||
[appApiAction]="formPromise"
|
||||
ngNativeValidate
|
||||
[bitSubmit]="submit"
|
||||
*ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans"
|
||||
class="tw-pt-6"
|
||||
>
|
||||
<bit-section>
|
||||
<app-org-info
|
||||
(changedBusinessOwned)="changedOwnedBusiness()"
|
||||
[formGroup]="formGroup"
|
||||
[createOrganization]="createOrganization"
|
||||
[isProvider]="!!providerId"
|
||||
[acceptingSponsorship]="acceptingSponsorship"
|
||||
></app-org-info>
|
||||
<h2 class="mt-5">{{ "chooseYourPlan" | i18n }}</h2>
|
||||
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="radio"
|
||||
name="product"
|
||||
id="product{{ selectableProduct.product }}"
|
||||
[value]="selectableProduct.product"
|
||||
formControlName="product"
|
||||
(change)="changedProduct()"
|
||||
/>
|
||||
<label class="form-check-label" for="product{{ selectableProduct.product }}">
|
||||
{{ selectableProduct.nameLocalizationKey | i18n }}
|
||||
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n: "1" }}</small>
|
||||
>
|
||||
</app-org-info>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "chooseYourPlan" | i18n }}</h2>
|
||||
<div *ngFor="let selectableProduct of selectableProducts">
|
||||
<bit-radio-group formControlName="product" [block]="true">
|
||||
<bit-radio-button [value]="selectableProduct.product" (change)="changedProduct()">
|
||||
<bit-label>{{ selectableProduct.nameLocalizationKey | i18n }}</bit-label>
|
||||
<bit-hint class="tw-text-sm"
|
||||
>{{ selectableProduct.descriptionLocalizationKey | i18n: "1" }}
|
||||
<ng-container
|
||||
*ngIf="selectableProduct.product === productTypes.Enterprise; else nonEnterprisePlans"
|
||||
>
|
||||
<small>• {{ "includeAllTeamsFeatures" | i18n }}</small>
|
||||
<small *ngIf="selectableProduct.hasSelfHost">• {{ "onPremHostingOptional" | i18n }}</small>
|
||||
<small *ngIf="selectableProduct.hasSso">• {{ "includeSsoAuthentication" | i18n }}</small>
|
||||
<small *ngIf="selectableProduct.hasPolicies"
|
||||
>• {{ "includeEnterprisePolicies" | i18n }}</small
|
||||
>
|
||||
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization"
|
||||
>•
|
||||
<ul class="tw-pl-0 tw-list-inside tw-mb-0">
|
||||
<li>{{ "includeAllTeamsFeatures" | i18n }}</li>
|
||||
<li *ngIf="selectableProduct.hasSelfHost">{{ "onPremHostingOptional" | i18n }}</li>
|
||||
<li *ngIf="selectableProduct.hasSso">{{ "includeSsoAuthentication" | i18n }}</li>
|
||||
<li *ngIf="selectableProduct.hasPolicies">
|
||||
{{ "includeEnterprisePolicies" | i18n }}
|
||||
</li>
|
||||
<li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
||||
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-template #nonEnterprisePlans>
|
||||
<ng-container
|
||||
*ngIf="selectableProduct.product === productTypes.Teams; else fullFeatureList"
|
||||
>
|
||||
<small>• {{ "includeAllTeamsStarterFeatures" | i18n }}</small>
|
||||
<small>• {{ "chooseMonthlyOrAnnualBilling" | i18n }}</small>
|
||||
<small>• {{ "abilityToAddMoreThanNMembers" | i18n: 10 }}</small>
|
||||
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
||||
• {{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
||||
</small>
|
||||
<ul class="tw-pl-0 tw-list-inside tw-mb-0">
|
||||
<li>{{ "includeAllTeamsStarterFeatures" | i18n }}</li>
|
||||
<li>{{ "chooseMonthlyOrAnnualBilling" | i18n }}</li>
|
||||
<li>{{ "abilityToAddMoreThanNMembers" | i18n: 10 }}</li>
|
||||
<li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
|
||||
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
|
||||
</li>
|
||||
</ul>
|
||||
</ng-container>
|
||||
<ng-template #fullFeatureList>
|
||||
<small *ngIf="selectableProduct.product == productTypes.Free"
|
||||
>• {{ "limitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}</small
|
||||
>
|
||||
<small
|
||||
<ul class="tw-pl-0 tw-list-inside tw-mb-0">
|
||||
<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 }}</small
|
||||
>
|
||||
<small *ngIf="!selectableProduct.PasswordManager.maxSeats"
|
||||
>• {{ "addShareUnlimitedUsers" | i18n }}</small
|
||||
>
|
||||
<small *ngIf="selectableProduct.PasswordManager.maxCollections"
|
||||
>•
|
||||
{{ "addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}
|
||||
</li>
|
||||
<li *ngIf="!selectableProduct.PasswordManager.maxSeats">
|
||||
{{ "addShareUnlimitedUsers" | i18n }}
|
||||
</li>
|
||||
<li *ngIf="selectableProduct.PasswordManager.maxCollections">
|
||||
{{
|
||||
"limitedCollections" | i18n: selectableProduct.PasswordManager.maxCollections
|
||||
}}</small
|
||||
>
|
||||
<small *ngIf="selectableProduct.PasswordManager.maxAdditionalSeats"
|
||||
>•
|
||||
}}
|
||||
</li>
|
||||
<li *ngIf="selectableProduct.PasswordManager.maxAdditionalSeats">
|
||||
{{
|
||||
"addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxAdditionalSeats
|
||||
}}</small
|
||||
>
|
||||
<small *ngIf="!selectableProduct.PasswordManager.maxCollections"
|
||||
>• {{ "createUnlimitedCollections" | i18n }}</small
|
||||
>
|
||||
<small *ngIf="selectableProduct.PasswordManager.baseStorageGb"
|
||||
>•
|
||||
"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"
|
||||
}}</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"
|
||||
>•
|
||||
}}
|
||||
</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 }}
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
<span *ngIf="selectableProduct.product != productTypes.Free">
|
||||
<ng-container *ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship">
|
||||
<ng-container
|
||||
*ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship"
|
||||
>
|
||||
{{
|
||||
(selectableProduct.isAnnual
|
||||
? selectableProduct.PasswordManager.basePrice / 12
|
||||
|
@ -174,113 +185,110 @@
|
|||
}}
|
||||
/{{ "month" | i18n }}
|
||||
</span>
|
||||
<span *ngIf="selectableProduct.product == productTypes.Free">{{ "freeForever" | i18n }}</span>
|
||||
</label>
|
||||
<span *ngIf="selectableProduct.product == productTypes.Free">{{
|
||||
"freeForever" | i18n
|
||||
}}</span>
|
||||
</bit-radio-group>
|
||||
</div>
|
||||
<div *ngIf="formGroup.value.product !== productTypes.Free">
|
||||
<ng-container
|
||||
</bit-section>
|
||||
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
|
||||
<bit-section
|
||||
*ngIf="
|
||||
selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
|
||||
!selectedPlan.PasswordManager.baseSeats
|
||||
"
|
||||
>
|
||||
<h2 class="mt-5">{{ "users" | i18n }}</h2>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<label for="additionalSeats">{{ "userSeats" | i18n }}</label>
|
||||
<h2 bitTypography="h2">{{ "users" | i18n }}</h2>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "userSeats" | i18n }}</bit-label>
|
||||
<input
|
||||
id="additionalSeats"
|
||||
class="form-control"
|
||||
bitInput
|
||||
type="number"
|
||||
name="additionalSeats"
|
||||
formControlName="additionalSeats"
|
||||
placeholder="{{ 'userSeatsDesc' | i18n }}"
|
||||
required
|
||||
/>
|
||||
<small class="text-muted form-text">{{ "userSeatsHowManyDesc" | i18n }}</small>
|
||||
<bit-hint class="tw-text-sm">{{ "userSeatsHowManyDesc" | i18n }}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<h2 class="mt-5">{{ "addons" | i18n }}</h2>
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
|
||||
<div
|
||||
class="row"
|
||||
class="tw-grid tw-grid-cols-12 tw-gap-4"
|
||||
*ngIf="
|
||||
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">
|
||||
<bit-label>{{ "additionalUserSeats" | i18n }}</bit-label>
|
||||
<input
|
||||
id="additionalSeats"
|
||||
class="form-control"
|
||||
bitInput
|
||||
type="number"
|
||||
name="additionalSeats"
|
||||
formControlName="additionalSeats"
|
||||
placeholder="{{ 'userSeatsDesc' | i18n }}"
|
||||
/>
|
||||
<small class="text-muted form-text">{{
|
||||
<bit-hint class="tx-text-sm"
|
||||
>{{
|
||||
"userSeatsAdditionalDesc"
|
||||
| i18n
|
||||
: selectedPlan.PasswordManager.baseSeats
|
||||
: (seatPriceMonthly(selectedPlan) | currency: "$")
|
||||
}}</small>
|
||||
}}
|
||||
</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-6">
|
||||
<label for="additionalStorage">{{ "additionalStorageGb" | i18n }}</label>
|
||||
<div class="tw-grid tw-grid-cols-12 tw-gap-4">
|
||||
<bit-form-field class="tw-col-span-6">
|
||||
<bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
|
||||
<input
|
||||
id="additionalStorage"
|
||||
class="form-control"
|
||||
bitInput
|
||||
type="number"
|
||||
name="additionalStorageGb"
|
||||
formControlName="additionalStorage"
|
||||
step="1"
|
||||
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
|
||||
/>
|
||||
<small class="text-muted form-text">{{
|
||||
<bit-hint class="tw-text-sm">{{
|
||||
"additionalStorageIntervalDesc"
|
||||
| i18n
|
||||
: "1 GB"
|
||||
: (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
|
||||
: ("month" | i18n)
|
||||
}}</small>
|
||||
}}</bit-hint>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="form-group col-6" *ngIf="selectedPlan.PasswordManager.hasPremiumAccessOption">
|
||||
<div class="form-check">
|
||||
<input
|
||||
id="premiumAccess"
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="premiumAccessAddon"
|
||||
formControlName="premiumAccessAddon"
|
||||
/>
|
||||
<label for="premiumAccess" class="form-check-label bold">{{
|
||||
"premiumAccess" | i18n
|
||||
}}</label>
|
||||
</div>
|
||||
<small class="text-muted form-text">{{
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<div
|
||||
class="tw-grid tw-grid-cols-12 tw-gap-4"
|
||||
*ngIf="selectedPlan.PasswordManager.hasPremiumAccessOption"
|
||||
>
|
||||
<bit-form-control class="tw-col-span-6">
|
||||
<bit-label>{{ "premiumAccess" | i18n }}</bit-label>
|
||||
<input type="checkbox" bitCheckbox formControlName="premiumAccessAddon" />
|
||||
<bit-hint class="tw-text-sm">{{
|
||||
"premiumAccessDesc" | i18n: (3.33 | currency: "$") : ("month" | i18n)
|
||||
}}</small>
|
||||
}}</bit-hint>
|
||||
</bit-form-control>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="spaced-header">{{ "summary" | i18n }}</h2>
|
||||
<div class="form-check form-check-block" *ngFor="let selectablePlan of selectablePlans">
|
||||
<input
|
||||
class="form-check-input"
|
||||
</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"
|
||||
name="plan"
|
||||
id="interval{{ selectablePlan.type }}"
|
||||
[value]="selectablePlan.type"
|
||||
formControlName="plan"
|
||||
/>
|
||||
<label class="form-check-label" for="interval{{ selectablePlan.type }}">
|
||||
<ng-container *ngIf="selectablePlan.isAnnual">
|
||||
{{ "annually" | i18n }}
|
||||
<small *ngIf="selectablePlan.PasswordManager.basePrice">
|
||||
>
|
||||
<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
|
||||
|
@ -292,7 +300,7 @@
|
|||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
|
||||
<span style="text-decoration: line-through">{{
|
||||
<span class="tw-line-through">{{
|
||||
selectablePlan.PasswordManager.basePrice | currency: "$"
|
||||
}}</span>
|
||||
{{ "freeWithSponsorship" | i18n }}
|
||||
|
@ -301,8 +309,12 @@
|
|||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
/{{ "year" | i18n }}
|
||||
</ng-template>
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption">
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||
>
|
||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||
>{{ "additionalUsers" | i18n }}:</span
|
||||
>
|
||||
|
@ -320,8 +332,12 @@
|
|||
| currency: "$"
|
||||
}}
|
||||
/{{ "year" | i18n }}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption">
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
|
||||
>
|
||||
{{ "additionalStorageGb" | i18n }}:
|
||||
{{ formGroup.controls["additionalStorage"].value || 0 }} ×
|
||||
{{
|
||||
|
@ -332,19 +348,26 @@
|
|||
}}
|
||||
× 12 {{ "monthAbbr" | i18n }} =
|
||||
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
|
||||
</small>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!selectablePlan.isAnnual">
|
||||
{{ "monthly" | i18n }}
|
||||
<small *ngIf="selectablePlan.PasswordManager.basePrice">
|
||||
</p>
|
||||
</bit-hint>
|
||||
<bit-hint *ngIf="!selectablePlan.isAnnual">
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.basePrice"
|
||||
>
|
||||
{{ "basePrice" | i18n }}:
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
{{ "monthAbbr" | i18n }}
|
||||
=
|
||||
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
|
||||
/{{ "month" | i18n }}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption">
|
||||
</p>
|
||||
<p
|
||||
class="tw-mb-0"
|
||||
bitTypography="body2"
|
||||
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
|
||||
>
|
||||
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
|
||||
>{{ "additionalUsers" | i18n }}:</span
|
||||
>
|
||||
|
@ -357,44 +380,49 @@
|
|||
| currency: "$"
|
||||
}}
|
||||
/{{ "month" | i18n }}
|
||||
</small>
|
||||
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption">
|
||||
</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 }}
|
||||
</small>
|
||||
</ng-container>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
</bit-hint>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</bit-section>
|
||||
</bit-section>
|
||||
|
||||
<!-- Secrets Manager -->
|
||||
<div class="tw-my-10">
|
||||
<bit-section>
|
||||
<sm-subscribe
|
||||
*ngIf="planOffersSecretsManager && !hasProvider"
|
||||
[formGroup]="formGroup.controls.secretsManager"
|
||||
[selectedPlan]="selectedSecretsManagerPlan"
|
||||
[upgradeOrganization]="!createOrganization"
|
||||
></sm-subscribe>
|
||||
</div>
|
||||
</bit-section>
|
||||
|
||||
<!-- Payment info -->
|
||||
<div *ngIf="formGroup.value.product !== productTypes.Free">
|
||||
<h2 class="mb-4">
|
||||
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
|
||||
<h2 bitTypography="h2">
|
||||
{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}
|
||||
</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 }}
|
||||
</small>
|
||||
</p>
|
||||
<app-payment
|
||||
*ngIf="createOrganization || upgradeRequiresPaymentMethod"
|
||||
[hideCredit]="true"
|
||||
></app-payment>
|
||||
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
|
||||
<div id="price" class="my-4">
|
||||
<div class="text-muted text-sm">
|
||||
<div id="price" class="tw-my-4">
|
||||
<div class="tw-text-muted tw-text-base">
|
||||
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }}
|
||||
<br />
|
||||
<span *ngIf="planOffersSecretsManager && formGroup.value.secretsManager.enabled">
|
||||
|
@ -405,8 +433,8 @@
|
|||
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
|
||||
</ng-container>
|
||||
</div>
|
||||
<hr class="my-1 col-3 ml-0" />
|
||||
<p class="text-lg">
|
||||
<hr class="tw-my-1 tw-grid tw-grid-cols-3 tw-ml-0" />
|
||||
<p class="tw-text-lg">
|
||||
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{
|
||||
selectedPlanInterval | i18n
|
||||
}}
|
||||
|
@ -415,22 +443,29 @@
|
|||
<ng-container *ngIf="!createOrganization">
|
||||
<app-payment [showMethods]="false"></app-payment>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="singleOrgPolicyBlock" class="mt-4">
|
||||
</bit-section>
|
||||
<bit-section *ngIf="singleOrgPolicyBlock">
|
||||
<app-callout [type]="'error'">{{ "singleOrgBlockCreateMessage" | i18n }}</app-callout>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
</bit-section>
|
||||
<bit-section>
|
||||
<button
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
bitButton
|
||||
[loading]="form.loading"
|
||||
bitFormButton
|
||||
[disabled]="!formGroup.valid"
|
||||
>
|
||||
{{ "submit" | i18n }}
|
||||
</button>
|
||||
<button type="button" buttonType="secondary" bitButton (click)="cancel()" *ngIf="showCancel">
|
||||
<button
|
||||
type="button"
|
||||
buttonType="secondary"
|
||||
bitButton
|
||||
bitFormButton
|
||||
(click)="cancel()"
|
||||
*ngIf="showCancel"
|
||||
>
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</bit-section>
|
||||
</form>
|
||||
|
|
|
@ -70,6 +70,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||
@Input() showCancel = false;
|
||||
@Input() acceptingSponsorship = false;
|
||||
@Input() currentPlan: PlanResponse;
|
||||
selectedFile: File;
|
||||
|
||||
@Input()
|
||||
get product(): ProductType {
|
||||
|
@ -109,6 +110,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||
|
||||
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
|
||||
|
||||
selfHostedForm = this.formBuilder.group({
|
||||
file: [null, [Validators.required]],
|
||||
});
|
||||
|
||||
formGroup = this.formBuilder.group({
|
||||
name: [""],
|
||||
billingEmail: ["", [Validators.email]],
|
||||
|
@ -527,12 +532,15 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||
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) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const doSubmit = async (): Promise<string> => {
|
||||
let orgId: string = null;
|
||||
if (this.createOrganization) {
|
||||
|
@ -587,12 +595,8 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||
this.formPromise = doSubmit();
|
||||
const organizationId = await this.formPromise;
|
||||
this.onSuccess.emit({ organizationId: organizationId });
|
||||
// TODO: No one actually listening to this message?
|
||||
this.messagingService.send("organizationCreated", { organizationId });
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
this.messagingService.send("organizationCreated", organizationId);
|
||||
};
|
||||
|
||||
private async updateOrganization(orgId: string) {
|
||||
const request = new OrganizationUpgradeRequest();
|
||||
|
@ -693,14 +697,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
|
||||
const fileEl = document.getElementById("file") as HTMLInputElement;
|
||||
const files = fileEl.files;
|
||||
if (files == null || files.length === 0) {
|
||||
if (!this.selectedFile) {
|
||||
throw new Error(this.i18nService.t("selectFile"));
|
||||
}
|
||||
|
||||
const fd = new FormData();
|
||||
fd.append("license", files[0]);
|
||||
fd.append("license", this.selectedFile);
|
||||
fd.append("key", key);
|
||||
fd.append("collectionName", collectionCt);
|
||||
const response = await this.organizationApiService.createLicense(fd);
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
{{ "addOrganization" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
|
||||
{{ "close" | i18n }}
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</bit-dialog>
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Subject, takeUntil } from "rxjs";
|
|||
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 { 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 { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
|
@ -45,6 +46,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
|
|||
protected stateService: StateService,
|
||||
protected dialogService: DialogService,
|
||||
protected kdfConfigService: KdfConfigService,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Router } from "@angular/router";
|
|||
import { firstValueFrom, Subject } from "rxjs";
|
||||
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 { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.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 { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
|
||||
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 { UserId } from "@bitwarden/common/types/guid";
|
||||
import { UserKey } from "@bitwarden/common/types/key";
|
||||
|
@ -55,7 +54,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||
protected onSuccessfulSubmit: () => Promise<void>;
|
||||
|
||||
private invalidPinAttempts = 0;
|
||||
private pinStatus: PinLockType;
|
||||
private pinLockType: PinLockType;
|
||||
|
||||
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
|
||||
|
||||
|
@ -81,7 +80,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||
protected dialogService: DialogService,
|
||||
protected deviceTrustService: DeviceTrustServiceAbstraction,
|
||||
protected userVerificationService: UserVerificationService,
|
||||
protected pinCryptoService: PinCryptoServiceAbstraction,
|
||||
protected pinService: PinServiceAbstraction,
|
||||
protected biometricStateService: BiometricStateService,
|
||||
protected accountService: AccountService,
|
||||
protected authService: AuthService,
|
||||
|
@ -168,7 +167,8 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
|
||||
|
||||
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) {
|
||||
await this.setUserKeyAndContinue(userKey);
|
||||
|
@ -272,7 +272,7 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||
return;
|
||||
}
|
||||
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||
await this.masterPasswordService.setMasterKey(masterKey, userId);
|
||||
await this.setUserKeyAndContinue(userKey, true);
|
||||
}
|
||||
|
@ -358,12 +358,13 @@ export class LockComponent implements OnInit, OnDestroy {
|
|||
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.pinStatus === "TRANSIENT" && !!ephemeralPinSet) || this.pinStatus === "PERSISTANT";
|
||||
(this.pinLockType === "EPHEMERAL" && !!ephemeralPinSet) || this.pinLockType === "PERSISTENT";
|
||||
|
||||
this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword();
|
||||
|
||||
this.supportsBiometric = await this.platformUtilsService.supportsBiometric();
|
||||
|
|
|
@ -52,7 +52,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
|||
|
||||
constructor(
|
||||
private accountService: AccountService,
|
||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
i18nService: I18nService,
|
||||
cryptoService: CryptoService,
|
||||
messagingService: MessagingService,
|
||||
|
@ -82,6 +82,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
|
|||
stateService,
|
||||
dialogService,
|
||||
kdfConfigService,
|
||||
masterPasswordService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,63 +1,66 @@
|
|||
import { DialogRef } from "@angular/cdk/dialog";
|
||||
import { Directive, OnInit } from "@angular/core";
|
||||
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 { 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";
|
||||
|
||||
@Directive()
|
||||
export class SetPinComponent implements OnInit {
|
||||
showMasterPassOnRestart = true;
|
||||
showMasterPasswordOnClientRestartOption = true;
|
||||
|
||||
setPinForm = this.formBuilder.group({
|
||||
pin: ["", [Validators.required]],
|
||||
masterPassOnRestart: true,
|
||||
requireMasterPasswordOnClientRestart: true,
|
||||
});
|
||||
|
||||
constructor(
|
||||
private dialogRef: DialogRef,
|
||||
private accountService: AccountService,
|
||||
private cryptoService: CryptoService,
|
||||
private userVerificationService: UserVerificationService,
|
||||
private stateService: StateService,
|
||||
private dialogRef: DialogRef,
|
||||
private formBuilder: FormBuilder,
|
||||
private kdfConfigService: KdfConfigService,
|
||||
private pinService: PinServiceAbstraction,
|
||||
private userVerificationService: UserVerificationService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
|
||||
|
||||
this.setPinForm.controls.masterPassOnRestart.setValue(hasMasterPassword);
|
||||
this.showMasterPassOnRestart = hasMasterPassword;
|
||||
this.setPinForm.controls.requireMasterPasswordOnClientRestart.setValue(hasMasterPassword);
|
||||
this.showMasterPasswordOnClientRestartOption = hasMasterPassword;
|
||||
}
|
||||
|
||||
submit = async () => {
|
||||
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)) {
|
||||
this.dialogRef.close(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const pinKey = await this.cryptoService.makePinKey(
|
||||
pin,
|
||||
await this.stateService.getEmail(),
|
||||
await this.kdfConfigService.getKdfConfig(),
|
||||
);
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
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) {
|
||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(pinProtectedKey);
|
||||
} else {
|
||||
await this.stateService.setPinKeyEncryptedUserKey(pinProtectedKey);
|
||||
}
|
||||
const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
|
||||
pin,
|
||||
userKey,
|
||||
userId,
|
||||
);
|
||||
await this.pinService.storePinKeyEncryptedUserKey(
|
||||
pinKeyEncryptedUserKey,
|
||||
requireMasterPasswordOnClientRestart,
|
||||
userId,
|
||||
);
|
||||
|
||||
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 { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||
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 { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
|
||||
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
|
||||
|
@ -46,6 +47,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
|||
private logService: LogService,
|
||||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
|
@ -57,6 +59,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
|
|||
stateService,
|
||||
dialogService,
|
||||
kdfConfigService,
|
||||
masterPasswordService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
|
|||
dialogService: DialogService,
|
||||
kdfConfigService: KdfConfigService,
|
||||
private accountService: AccountService,
|
||||
private masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
) {
|
||||
super(
|
||||
i18nService,
|
||||
|
@ -74,6 +74,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
|
|||
stateService,
|
||||
dialogService,
|
||||
kdfConfigService,
|
||||
masterPasswordService,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ import { Subject } from "rxjs";
|
|||
import {
|
||||
AuthRequestServiceAbstraction,
|
||||
AuthRequestService,
|
||||
PinCryptoServiceAbstraction,
|
||||
PinCryptoService,
|
||||
PinServiceAbstraction,
|
||||
PinService,
|
||||
LoginStrategyServiceAbstraction,
|
||||
LoginStrategyService,
|
||||
LoginEmailServiceAbstraction,
|
||||
|
@ -545,6 +545,7 @@ const safeProviders: SafeProvider[] = [
|
|||
provide: CryptoServiceAbstraction,
|
||||
useClass: CryptoService,
|
||||
deps: [
|
||||
PinServiceAbstraction,
|
||||
InternalMasterPasswordServiceAbstraction,
|
||||
KeyGenerationServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
|
@ -661,6 +662,8 @@ const safeProviders: SafeProvider[] = [
|
|||
provide: VaultTimeoutSettingsServiceAbstraction,
|
||||
useClass: VaultTimeoutSettingsService,
|
||||
deps: [
|
||||
AccountServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
|
@ -728,6 +731,7 @@ const safeProviders: SafeProvider[] = [
|
|||
I18nServiceAbstraction,
|
||||
CollectionServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
@ -736,6 +740,7 @@ const safeProviders: SafeProvider[] = [
|
|||
deps: [
|
||||
FolderServiceAbstraction,
|
||||
CipherServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
KdfConfigServiceAbstraction,
|
||||
|
@ -747,6 +752,7 @@ const safeProviders: SafeProvider[] = [
|
|||
deps: [
|
||||
CipherServiceAbstraction,
|
||||
ApiServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
CollectionServiceAbstraction,
|
||||
|
@ -822,7 +828,7 @@ const safeProviders: SafeProvider[] = [
|
|||
safeProvider({
|
||||
provide: InternalMasterPasswordServiceAbstraction,
|
||||
useClass: MasterPasswordService,
|
||||
deps: [StateProvider],
|
||||
deps: [StateProvider, StateServiceAbstraction, KeyGenerationServiceAbstraction, EncryptService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: MasterPasswordServiceAbstraction,
|
||||
|
@ -855,7 +861,7 @@ const safeProviders: SafeProvider[] = [
|
|||
I18nServiceAbstraction,
|
||||
UserVerificationApiServiceAbstraction,
|
||||
UserDecryptionOptionsServiceAbstraction,
|
||||
PinCryptoServiceAbstraction,
|
||||
PinServiceAbstraction,
|
||||
LogService,
|
||||
VaultTimeoutSettingsServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
|
@ -1005,14 +1011,18 @@ const safeProviders: SafeProvider[] = [
|
|||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: PinCryptoServiceAbstraction,
|
||||
useClass: PinCryptoService,
|
||||
provide: PinServiceAbstraction,
|
||||
useClass: PinService,
|
||||
deps: [
|
||||
StateServiceAbstraction,
|
||||
CryptoServiceAbstraction,
|
||||
VaultTimeoutSettingsServiceAbstraction,
|
||||
LogService,
|
||||
AccountServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
EncryptService,
|
||||
KdfConfigServiceAbstraction,
|
||||
KeyGenerationServiceAbstraction,
|
||||
LogService,
|
||||
MasterPasswordServiceAbstraction,
|
||||
StateProvider,
|
||||
StateServiceAbstraction,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
</h1>
|
||||
<p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p>
|
||||
</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
|
||||
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-strategy.service";
|
||||
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;
|
||||
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId });
|
||||
|
||||
await authRequestLoginStrategy.logIn(credentials);
|
||||
|
|
|
@ -156,7 +156,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -262,7 +262,7 @@ describe("LoginStrategy", () => {
|
|||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
const result = await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
|
@ -281,7 +281,7 @@ describe("LoginStrategy", () => {
|
|||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
||||
|
|
|
@ -163,7 +163,7 @@ describe("PasswordLoginStrategy", () => {
|
|||
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
|
||||
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
|
||||
|
||||
await passwordLoginStrategy.logIn(credentials);
|
||||
|
|
|
@ -228,7 +228,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey, userId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -445,11 +445,15 @@ describe("SsoLoginStrategy", () => {
|
|||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
masterKey,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
||||
});
|
||||
});
|
||||
|
@ -497,11 +501,15 @@ describe("SsoLoginStrategy", () => {
|
|||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
await ssoLoginStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
masterKey,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -350,7 +350,7 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||
return;
|
||||
}
|
||||
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey);
|
||||
}
|
||||
|
||||
|
|
|
@ -190,11 +190,15 @@ describe("UserApiLoginStrategy", () => {
|
|||
|
||||
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||
masterPasswordService.masterKeySubject.next(masterKey);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey);
|
||||
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
masterKey,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -110,7 +110,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
|||
if (response.apiUseKeyConnector) {
|
||||
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
|
||||
if (masterKey) {
|
||||
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey);
|
||||
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
|
||||
await this.cryptoService.setUserKey(userKey, userId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -172,7 +172,9 @@ describe("AuthRequestService", () => {
|
|||
|
||||
masterPasswordService.masterKeySubject.next(undefined);
|
||||
masterPasswordService.masterKeyHashSubject.next(undefined);
|
||||
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey);
|
||||
masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(
|
||||
mockDecryptedUserKey,
|
||||
);
|
||||
cryptoService.setUserKey.mockResolvedValueOnce(undefined);
|
||||
|
||||
// Act
|
||||
|
@ -192,8 +194,10 @@ describe("AuthRequestService", () => {
|
|||
mockDecryptedMasterKeyHash,
|
||||
mockUserId,
|
||||
);
|
||||
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
|
||||
mockDecryptedMasterKey,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey);
|
||||
});
|
||||
|
|
|
@ -178,7 +178,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
|
|||
);
|
||||
|
||||
// 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)
|
||||
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-strategies/login-strategy.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 { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||
import { PinLockType } from "../../services/vault-timeout/vault-timeout-settings.service";
|
||||
|
||||
export abstract class VaultTimeoutSettingsService {
|
||||
/**
|
||||
|
@ -38,13 +37,6 @@ export abstract class VaultTimeoutSettingsService {
|
|||
*/
|
||||
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.
|
||||
* @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 { UserId } from "../../types/guid";
|
||||
import { MasterKey } from "../../types/key";
|
||||
import { MasterKey, UserKey } from "../../types/key";
|
||||
import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
|
||||
|
||||
export abstract class MasterPasswordServiceAbstraction {
|
||||
|
@ -30,6 +30,20 @@ export abstract class MasterPasswordServiceAbstraction {
|
|||
* @throws If the user ID is missing.
|
||||
*/
|
||||
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 {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ReplaySubject, Observable } from "rxjs";
|
|||
|
||||
import { EncString } from "../../../platform/models/domain/enc-string";
|
||||
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 { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
|
||||
|
||||
|
@ -61,4 +61,12 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
|
|||
setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> {
|
||||
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 { 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 { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
|
||||
import {
|
||||
|
@ -9,7 +13,7 @@ import {
|
|||
UserKeyDefinition,
|
||||
} from "../../../platform/state";
|
||||
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 { 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 {
|
||||
constructor(private stateProvider: StateProvider) {}
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private stateService: StateService,
|
||||
private keyGenerationService: KeyGenerationService,
|
||||
private encryptService: EncryptService,
|
||||
) {}
|
||||
|
||||
masterKey$(userId: UserId): Observable<MasterKey> {
|
||||
if (userId == null) {
|
||||
|
@ -137,4 +146,48 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
|
|||
}
|
||||
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 { 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 { CryptoService } from "../../../platform/abstractions/crypto.service";
|
||||
import { I18nService } from "../../../platform/abstractions/i18n.service";
|
||||
|
@ -44,7 +44,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
|||
private i18nService: I18nService,
|
||||
private userVerificationApiService: UserVerificationApiServiceAbstraction,
|
||||
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
|
||||
private pinCryptoService: PinCryptoServiceAbstraction,
|
||||
private pinService: PinServiceAbstraction,
|
||||
private logService: LogService,
|
||||
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
|
@ -55,10 +55,11 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
|||
verificationType: keyof UserVerificationOptions,
|
||||
): Promise<UserVerificationOptions> {
|
||||
if (verificationType === "client") {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
const [userHasMasterPassword, pinLockType, biometricsLockSet, biometricsUserKeyStored] =
|
||||
await Promise.all([
|
||||
this.hasMasterPasswordAndMasterKeyHash(),
|
||||
this.vaultTimeoutSettingsService.isPinLockSet(),
|
||||
this.pinService.getPinLockType(userId),
|
||||
this.vaultTimeoutSettingsService.isBiometricLockSet(),
|
||||
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)
|
||||
*/
|
||||
async verifyUser(verification: Verification): Promise<boolean> {
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
|
||||
if (verificationHasSecret(verification)) {
|
||||
this.validateSecretInput(verification);
|
||||
}
|
||||
|
@ -145,9 +148,9 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
|||
case VerificationType.OTP:
|
||||
return this.verifyUserByOTP(verification);
|
||||
case VerificationType.MasterPassword:
|
||||
return this.verifyUserByMasterPassword(verification);
|
||||
return this.verifyUserByMasterPassword(verification, userId);
|
||||
case VerificationType.PIN:
|
||||
return this.verifyUserByPIN(verification);
|
||||
return this.verifyUserByPIN(verification, userId);
|
||||
case VerificationType.Biometrics:
|
||||
return this.verifyUserByBiometrics();
|
||||
default: {
|
||||
|
@ -170,8 +173,12 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
|||
|
||||
private async verifyUserByMasterPassword(
|
||||
verification: MasterPasswordVerification,
|
||||
userId: UserId,
|
||||
): 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));
|
||||
if (!masterKey) {
|
||||
masterKey = await this.cryptoService.makeMasterKey(
|
||||
|
@ -192,8 +199,12 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
|
|||
return true;
|
||||
}
|
||||
|
||||
private async verifyUserByPIN(verification: PinVerification): Promise<boolean> {
|
||||
const userKey = await this.pinCryptoService.decryptUserKeyWithPin(verification.secret);
|
||||
private async verifyUserByPIN(verification: PinVerification, userId: UserId): Promise<boolean> {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ export class WebAuthnLoginPrfCryptoService implements WebAuthnLoginPrfCryptoServ
|
|||
return (await this.stretchKey(new Uint8Array(prf))) as PrfKey;
|
||||
}
|
||||
|
||||
// TODO: use keyGenerationService.stretchKey
|
||||
private async stretchKey(key: Uint8Array): Promise<SymmetricCryptoKey> {
|
||||
const newKey = new Uint8Array(64);
|
||||
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 { KdfConfig } from "../../auth/models/domain/kdf-config";
|
||||
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 { EncArrayBuffer } from "../models/domain/enc-array-buffer";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
|
@ -139,18 +139,6 @@ export abstract class CryptoService {
|
|||
masterKey: MasterKey,
|
||||
userKey?: UserKey,
|
||||
): 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
|
||||
* 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.
|
||||
*/
|
||||
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
|
||||
* Note: This will remove the stored pin and as a result,
|
||||
|
@ -282,39 +263,6 @@ export abstract class CryptoService {
|
|||
* @param userId The desired user
|
||||
*/
|
||||
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
|
||||
* @returns A new send key
|
||||
|
@ -358,16 +306,6 @@ export abstract class CryptoService {
|
|||
publicKey: string;
|
||||
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.
|
||||
* 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,
|
||||
kdfConfig: KdfConfig,
|
||||
): 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 { UserId } from "../../types/guid";
|
||||
import { Account } from "../models/domain/account";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
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
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
@ -101,14 +80,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||
value: GeneratedPasswordHistory[],
|
||||
options?: StorageOptions,
|
||||
) => 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>;
|
||||
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEmail: (options?: StorageOptions) => Promise<string>;
|
||||
|
@ -127,14 +98,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||
value: GeneratedPasswordHistory[],
|
||||
options?: StorageOptions,
|
||||
) => 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>;
|
||||
getLastSync: (options?: StorageOptions) => Promise<string>;
|
||||
setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
|
@ -154,14 +117,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||
) => Promise<void>;
|
||||
getGeneratorOptions: (options?: StorageOptions) => Promise<GeneratorOptions>;
|
||||
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>;
|
||||
getVaultTimeout: (options?: StorageOptions) => Promise<number>;
|
||||
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
|
|
|
@ -1,24 +1,9 @@
|
|||
import { AccountSettings, EncryptionPair } from "./account";
|
||||
import { EncString } from "./enc-string";
|
||||
import { AccountSettings } from "./account";
|
||||
|
||||
describe("AccountSettings", () => {
|
||||
describe("fromJSON", () => {
|
||||
it("should deserialize to an instance of itself", () => {
|
||||
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 { Utils } from "../../misc/utils";
|
||||
|
||||
import { EncryptedString, EncString } from "./enc-string";
|
||||
import { SymmetricCryptoKey } from "./symmetric-crypto-key";
|
||||
|
||||
export class EncryptionPair<TEncrypted, TDecrypted> {
|
||||
|
@ -148,26 +147,15 @@ export class AccountSettings {
|
|||
passwordGenerationOptions?: PasswordGeneratorOptions;
|
||||
usernameGenerationOptions?: UsernameGeneratorOptions;
|
||||
generatorOptions?: GeneratorOptions;
|
||||
pinKeyEncryptedUserKey?: EncryptedString;
|
||||
pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
|
||||
protectedPin?: string;
|
||||
vaultTimeout?: number;
|
||||
vaultTimeoutAction?: string = "lock";
|
||||
|
||||
/** @deprecated July 2023, left for migration purposes*/
|
||||
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
|
||||
|
||||
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.assign(new AccountSettings(), obj, {
|
||||
pinProtected: EncryptionPair.fromJSON<string, EncString>(
|
||||
obj?.pinProtected,
|
||||
EncString.fromJSON,
|
||||
),
|
||||
});
|
||||
return Object.assign(new AccountSettings(), obj);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of, tap } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
|
||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
|
||||
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
|
||||
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 { CsprngArray } from "../../types/csprng";
|
||||
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 { EncryptService } from "../abstractions/encrypt.service";
|
||||
import { KeyGenerationService } from "../abstractions/key-generation.service";
|
||||
|
@ -32,6 +33,7 @@ import {
|
|||
describe("cryptoService", () => {
|
||||
let cryptoService: CryptoService;
|
||||
|
||||
const pinService = mock<PinServiceAbstraction>();
|
||||
const keyGenerationService = mock<KeyGenerationService>();
|
||||
const cryptoFunctionService = mock<CryptoFunctionService>();
|
||||
const encryptService = mock<EncryptService>();
|
||||
|
@ -51,6 +53,7 @@ describe("cryptoService", () => {
|
|||
stateProvider = new FakeStateProvider(accountService);
|
||||
|
||||
cryptoService = new CryptoService(
|
||||
pinService,
|
||||
masterPasswordService,
|
||||
keyGenerationService,
|
||||
cryptoFunctionService,
|
||||
|
@ -251,60 +254,50 @@ describe("cryptoService", () => {
|
|||
});
|
||||
|
||||
describe("Pin Key refresh", () => {
|
||||
let cryptoSvcMakePinKey: jest.SpyInstance;
|
||||
const protectedPin =
|
||||
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=";
|
||||
let encPin: EncString;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoSvcMakePinKey = jest.spyOn(cryptoService, "makePinKey");
|
||||
cryptoSvcMakePinKey.mockResolvedValue(new SymmetricCryptoKey(new Uint8Array(64)) as PinKey);
|
||||
encPin = new EncString(
|
||||
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
|
||||
const mockPinKeyEncryptedUserKey = new EncString(
|
||||
"2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
|
||||
);
|
||||
const mockUserKeyEncryptedPin = new EncString(
|
||||
"2.BBBw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
|
||||
);
|
||||
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=",
|
||||
),
|
||||
it("sets a pinKeyEncryptedUserKeyPersistent if a userKeyEncryptedPin and pinKeyEncryptedUserKey is set", async () => {
|
||||
pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
|
||||
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
|
||||
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(
|
||||
mockPinKeyEncryptedUserKey,
|
||||
);
|
||||
|
||||
await cryptoService.setUserKey(mockUserKey, mockUserId);
|
||||
|
||||
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(expect.any(EncString), {
|
||||
userId: 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,
|
||||
},
|
||||
expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||
mockPinKeyEncryptedUserKey,
|
||||
false,
|
||||
mockUserId,
|
||||
);
|
||||
});
|
||||
|
||||
it("clears the UserKeyPin and UserKeyPinEphemeral if the ProtectedPin is not set", async () => {
|
||||
stateService.getProtectedPin.mockResolvedValue(null);
|
||||
it("sets a pinKeyEncryptedUserKeyEphemeral if a userKeyEncryptedPin is set, but a pinKeyEncryptedUserKey is not set", async () => {
|
||||
pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
|
||||
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
|
||||
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(null);
|
||||
|
||||
await cryptoService.setUserKey(mockUserKey, mockUserId);
|
||||
|
||||
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(null, {
|
||||
userId: mockUserId,
|
||||
});
|
||||
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(null, {
|
||||
userId: mockUserId,
|
||||
expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
|
||||
mockPinKeyEncryptedUserKey,
|
||||
true,
|
||||
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 { 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 { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
|
||||
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
|
||||
|
@ -17,7 +18,6 @@ import {
|
|||
UserKey,
|
||||
MasterKey,
|
||||
ProviderKey,
|
||||
PinKey,
|
||||
CipherKey,
|
||||
UserPrivateKey,
|
||||
UserPublicKey,
|
||||
|
@ -74,6 +74,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
readonly everHadUserKey$: Observable<boolean>;
|
||||
|
||||
constructor(
|
||||
protected pinService: PinServiceAbstraction,
|
||||
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
|
||||
protected keyGenerationService: KeyGenerationService,
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
|
@ -254,7 +255,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
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.
|
||||
// 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.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
|
||||
|
@ -303,46 +304,6 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
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
|
||||
async hashMasterKey(
|
||||
password: string,
|
||||
|
@ -548,53 +509,19 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
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> {
|
||||
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId });
|
||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
|
||||
await this.stateService.setProtectedPin(null, { userId: userId });
|
||||
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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> {
|
||||
return await this.keyGenerationService.deriveKeyFromMaterial(
|
||||
keyMaterial,
|
||||
|
@ -798,6 +725,12 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
* @param userId The desired user
|
||||
*/
|
||||
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);
|
||||
if (storeAuto) {
|
||||
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);
|
||||
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
|
||||
// migrated once used to unlock
|
||||
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
|
||||
} else {
|
||||
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId });
|
||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: 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 });
|
||||
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
|
||||
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -851,8 +778,8 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
break;
|
||||
}
|
||||
case KeySuffixOptions.Pin: {
|
||||
const protectedPin = await this.stateService.getProtectedPin({ userId: userId });
|
||||
shouldStoreKey = !!protectedPin;
|
||||
const userKeyEncryptedPin = await this.pinService.getUserKeyEncryptedPin(userId);
|
||||
shouldStoreKey = !!userKeyEncryptedPin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -874,16 +801,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
|
||||
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
|
||||
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
|
||||
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: 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);
|
||||
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
}
|
||||
|
||||
private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) {
|
||||
|
@ -912,7 +830,7 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
): Promise<[T, EncString]> {
|
||||
let protectedSymKey: EncString = null;
|
||||
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);
|
||||
} else if (encryptionKey.key.byteLength === 64) {
|
||||
protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey);
|
||||
|
@ -931,42 +849,10 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||
if (keySuffix === KeySuffixOptions.Auto) {
|
||||
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
|
||||
} else if (keySuffix === KeySuffixOptions.Pin) {
|
||||
await this.stateService.setEncryptedPinProtected(null, { userId: userId });
|
||||
await this.stateService.setDecryptedPinProtected(null, { userId: userId });
|
||||
await this.pinService.clearOldPinKeyEncryptedMasterKey(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--
|
||||
|
||||
/**
|
||||
|
|
|
@ -81,4 +81,15 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction {
|
|||
}
|
||||
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 { Utils } from "../misc/utils";
|
||||
import { Account, AccountData, AccountSettings } from "../models/domain/account";
|
||||
import { EncString } from "../models/domain/enc-string";
|
||||
import { GlobalState } from "../models/domain/global-state";
|
||||
import { State } from "../models/domain/state";
|
||||
import { StorageOptions } from "../models/domain/storage-options";
|
||||
|
@ -220,45 +219,6 @@ export class StateService<
|
|||
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
|
||||
*/
|
||||
|
@ -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> {
|
||||
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
|
||||
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> {
|
||||
return (
|
||||
(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> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { firstValueFrom, map, timeout } from "rxjs";
|
||||
|
||||
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
|
||||
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
import { AuthService } from "../../auth/abstractions/auth.service";
|
||||
|
@ -20,6 +21,7 @@ export class SystemService implements SystemServiceAbstraction {
|
|||
private clearClipboardTimeoutFunction: () => Promise<any> = null;
|
||||
|
||||
constructor(
|
||||
private pinService: PinServiceAbstraction,
|
||||
private messagingService: MessagingService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
private reloadCallback: () => Promise<void> = null,
|
||||
|
@ -50,11 +52,14 @@ export class SystemService implements SystemServiceAbstraction {
|
|||
return;
|
||||
}
|
||||
|
||||
// User has set a PIN, with ask for master password on restart, to protect their vault
|
||||
const ephemeralPin = await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
|
||||
// If there is an active user, check if they have a pinKeyEncryptedUserKeyEphemeral. If so, prevent process reload upon lock.
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
if (userId != null) {
|
||||
const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId);
|
||||
if (ephemeralPin != null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.cancelProcessReload();
|
||||
await this.executeProcessReload();
|
||||
|
|
|
@ -35,31 +35,33 @@ export const BILLING_DISK = new StateDefinition("billing", "disk");
|
|||
|
||||
// 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 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", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
||||
export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
|
||||
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 TOKEN_DISK = new StateDefinition("token", "disk");
|
||||
export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const TOKEN_MEMORY = new StateDefinition("token", "memory");
|
||||
export const DEVICE_TRUST_DISK_LOCAL = new StateDefinition("deviceTrust", "disk", {
|
||||
web: "disk-local",
|
||||
});
|
||||
export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
|
||||
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
|
||||
|
||||
// Autofill
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue