Merge branch 'main' into auth/pm-7392/token-service-add-secure-storage-fallback

This commit is contained in:
Jared Snider 2024-05-09 13:11:00 -04:00 committed by GitHub
commit 5791be9143
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
115 changed files with 3337 additions and 1887 deletions

View File

@ -3044,6 +3044,12 @@
"accountSecurity": { "accountSecurity": {
"message": "Account security" "message": "Account security"
}, },
"notifications": {
"message": "Notifications"
},
"appearance": {
"message": "Appearance"
},
"errorAssigningTargetCollection": { "errorAssigningTargetCollection": {
"message": "Error assigning target collection." "message": "Error assigning target collection."
}, },

View File

@ -4,20 +4,35 @@ import {
} from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service"; import { MasterPasswordService } from "@bitwarden/common/auth/services/master-password/master-password.service";
import {
encryptServiceFactory,
EncryptServiceInitOptions,
} from "../../../platform/background/service-factories/encrypt-service.factory";
import { import {
CachedServices, CachedServices,
factory, factory,
FactoryOptions, FactoryOptions,
} from "../../../platform/background/service-factories/factory-options"; } from "../../../platform/background/service-factories/factory-options";
import {
keyGenerationServiceFactory,
KeyGenerationServiceInitOptions,
} from "../../../platform/background/service-factories/key-generation-service.factory";
import { import {
stateProviderFactory, stateProviderFactory,
StateProviderInitOptions, StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory"; } from "../../../platform/background/service-factories/state-provider.factory";
import {
stateServiceFactory,
StateServiceInitOptions,
} from "../../../platform/background/service-factories/state-service.factory";
type MasterPasswordServiceFactoryOptions = FactoryOptions; type MasterPasswordServiceFactoryOptions = FactoryOptions;
export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions & export type MasterPasswordServiceInitOptions = MasterPasswordServiceFactoryOptions &
StateProviderInitOptions; StateProviderInitOptions &
StateServiceInitOptions &
KeyGenerationServiceInitOptions &
EncryptServiceInitOptions;
export function internalMasterPasswordServiceFactory( export function internalMasterPasswordServiceFactory(
cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices, cache: { masterPasswordService?: InternalMasterPasswordServiceAbstraction } & CachedServices,
@ -27,7 +42,13 @@ export function internalMasterPasswordServiceFactory(
cache, cache,
"masterPasswordService", "masterPasswordService",
opts, opts,
async () => new MasterPasswordService(await stateProviderFactory(cache, opts)), async () =>
new MasterPasswordService(
await stateProviderFactory(cache, opts),
await stateServiceFactory(cache, opts),
await keyGenerationServiceFactory(cache, opts),
await encryptServiceFactory(cache, opts),
),
); );
} }

View File

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

View File

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

View File

@ -37,7 +37,7 @@ import {
internalMasterPasswordServiceFactory, internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions, MasterPasswordServiceInitOptions,
} from "./master-password-service.factory"; } from "./master-password-service.factory";
import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory"; import { PinServiceInitOptions, pinServiceFactory } from "./pin-service.factory";
import { import {
userDecryptionOptionsServiceFactory, userDecryptionOptionsServiceFactory,
UserDecryptionOptionsServiceInitOptions, UserDecryptionOptionsServiceInitOptions,
@ -57,7 +57,7 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO
I18nServiceInitOptions & I18nServiceInitOptions &
UserVerificationApiServiceInitOptions & UserVerificationApiServiceInitOptions &
UserDecryptionOptionsServiceInitOptions & UserDecryptionOptionsServiceInitOptions &
PinCryptoServiceInitOptions & PinServiceInitOptions &
LogServiceInitOptions & LogServiceInitOptions &
VaultTimeoutSettingsServiceInitOptions & VaultTimeoutSettingsServiceInitOptions &
PlatformUtilsServiceInitOptions & PlatformUtilsServiceInitOptions &
@ -80,7 +80,7 @@ export function userVerificationServiceFactory(
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await userVerificationApiServiceFactory(cache, opts), await userVerificationApiServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts), await userDecryptionOptionsServiceFactory(cache, opts),
await pinCryptoServiceFactory(cache, opts), await pinServiceFactory(cache, opts),
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await vaultTimeoutSettingsServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts),
await platformUtilsServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts),

View File

@ -12,8 +12,16 @@
<input class="tw-font-mono" bitInput type="password" formControlName="pin" /> <input class="tw-font-mono" bitInput type="password" formControlName="pin" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button> <button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field> </bit-form-field>
<label class="tw-flex tw-items-start tw-gap-2" *ngIf="showMasterPassOnRestart"> <label
<input class="tw-mt-1" type="checkbox" bitCheckbox formControlName="masterPassOnRestart" /> class="tw-flex tw-items-start tw-gap-2"
*ngIf="showMasterPasswordOnClientRestartOption"
>
<input
class="tw-mt-1"
type="checkbox"
bitCheckbox
formControlName="requireMasterPasswordOnClientRestart"
/>
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span> <span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
</label> </label>
</div> </div>

View File

@ -3,7 +3,7 @@ import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
@ -63,7 +63,7 @@ export class LockComponent extends BaseLockComponent {
dialogService: DialogService, dialogService: DialogService,
deviceTrustService: DeviceTrustServiceAbstraction, deviceTrustService: DeviceTrustServiceAbstraction,
userVerificationService: UserVerificationService, userVerificationService: UserVerificationService,
pinCryptoService: PinCryptoServiceAbstraction, pinService: PinServiceAbstraction,
private routerService: BrowserRouterService, private routerService: BrowserRouterService,
biometricStateService: BiometricStateService, biometricStateService: BiometricStateService,
accountService: AccountService, accountService: AccountService,
@ -89,7 +89,7 @@ export class LockComponent extends BaseLockComponent {
dialogService, dialogService,
deviceTrustService, deviceTrustService,
userVerificationService, userVerificationService,
pinCryptoService, pinService,
biometricStateService, biometricStateService,
accountService, accountService,
authService, authService,

View File

@ -16,6 +16,7 @@ import {
} from "rxjs"; } from "rxjs";
import { FingerprintDialogComponent } from "@bitwarden/auth/angular"; import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
@ -71,6 +72,7 @@ export class AccountSecurityComponent implements OnInit {
constructor( constructor(
private accountService: AccountService, private accountService: AccountService,
private pinService: PinServiceAbstraction,
private policyService: PolicyService, private policyService: PolicyService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
@ -131,7 +133,6 @@ export class AccountSecurityComponent implements OnInit {
if (timeout === -2 && !showOnLocked) { if (timeout === -2 && !showOnLocked) {
timeout = -1; timeout = -1;
} }
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet();
this.form.controls.vaultTimeout.valueChanges this.form.controls.vaultTimeout.valueChanges
.pipe( .pipe(
@ -153,12 +154,14 @@ export class AccountSecurityComponent implements OnInit {
) )
.subscribe(); .subscribe();
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const initialValues = { const initialValues = {
vaultTimeout: timeout, vaultTimeout: timeout,
vaultTimeoutAction: await firstValueFrom( vaultTimeoutAction: await firstValueFrom(
this.vaultTimeoutSettingsService.vaultTimeoutAction$(), this.vaultTimeoutSettingsService.vaultTimeoutAction$(),
), ),
pin: pinStatus !== "DISABLED", pin: await this.pinService.isPinSet(userId),
biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(), biometric: await this.vaultTimeoutSettingsService.isBiometricLockSet(),
enableAutoBiometricsPrompt: await firstValueFrom( enableAutoBiometricsPrompt: await firstValueFrom(
this.biometricStateService.promptAutomatically$, this.biometricStateService.promptAutomatically$,

View File

@ -1,7 +1,7 @@
<form #form (ngSubmit)="submit()"> <form #form (ngSubmit)="submit()">
<header> <header>
<div class="left"> <div class="left">
<button type="button" routerLink="/tabs/settings"> <button type="button" routerLink="/notifications">
<span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span> <span class="header-icon"><i class="bwi bwi-angle-left" aria-hidden="true"></i></span>
<span>{{ "back" | i18n }}</span> <span>{{ "back" | i18n }}</span>
</button> </button>

View File

@ -8,8 +8,8 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserApi } from "../../../platform/browser/browser-api";
import { enableAccountSwitching } from "../../platform/flags"; import { enableAccountSwitching } from "../../../platform/flags";
interface ExcludedDomain { interface ExcludedDomain {
uri: string; uri: string;

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs"; import { Subject, filter, firstValueFrom, map, merge, timeout } from "rxjs";
import { import {
PinCryptoServiceAbstraction, PinServiceAbstraction,
PinCryptoService, PinService,
InternalUserDecryptionOptionsServiceAbstraction, InternalUserDecryptionOptionsServiceAbstraction,
UserDecryptionOptionsService, UserDecryptionOptionsService,
AuthRequestServiceAbstraction, AuthRequestServiceAbstraction,
@ -319,7 +319,7 @@ export default class MainBackground {
authRequestService: AuthRequestServiceAbstraction; authRequestService: AuthRequestServiceAbstraction;
accountService: AccountServiceAbstraction; accountService: AccountServiceAbstraction;
globalStateProvider: GlobalStateProvider; globalStateProvider: GlobalStateProvider;
pinCryptoService: PinCryptoServiceAbstraction; pinService: PinServiceAbstraction;
singleUserStateProvider: SingleUserStateProvider; singleUserStateProvider: SingleUserStateProvider;
activeUserStateProvider: ActiveUserStateProvider; activeUserStateProvider: ActiveUserStateProvider;
derivedStateProvider: DerivedStateProvider; derivedStateProvider: DerivedStateProvider;
@ -544,13 +544,31 @@ export default class MainBackground {
const themeStateService = new DefaultThemeStateService(this.globalStateProvider); const themeStateService = new DefaultThemeStateService(this.globalStateProvider);
this.masterPasswordService = new MasterPasswordService(this.stateProvider); this.masterPasswordService = new MasterPasswordService(
this.stateProvider,
this.stateService,
this.keyGenerationService,
this.encryptService,
);
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider); this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
this.kdfConfigService = new KdfConfigService(this.stateProvider); this.kdfConfigService = new KdfConfigService(this.stateProvider);
this.pinService = new PinService(
this.accountService,
this.cryptoFunctionService,
this.encryptService,
this.kdfConfigService,
this.keyGenerationService,
this.logService,
this.masterPasswordService,
this.stateProvider,
this.stateService,
);
this.cryptoService = new BrowserCryptoService( this.cryptoService = new BrowserCryptoService(
this.pinService,
this.masterPasswordService, this.masterPasswordService,
this.keyGenerationService, this.keyGenerationService,
this.cryptoFunctionService, this.cryptoFunctionService,
@ -705,6 +723,8 @@ export default class MainBackground {
this.folderApiService = new FolderApiService(this.folderService, this.apiService); this.folderApiService = new FolderApiService(this.folderService, this.apiService);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.accountService,
this.pinService,
this.userDecryptionOptionsService, this.userDecryptionOptionsService,
this.cryptoService, this.cryptoService,
this.tokenService, this.tokenService,
@ -713,14 +733,6 @@ export default class MainBackground {
this.biometricStateService, this.biometricStateService,
); );
this.pinCryptoService = new PinCryptoService(
this.stateService,
this.cryptoService,
this.vaultTimeoutSettingsService,
this.logService,
this.kdfConfigService,
);
this.userVerificationService = new UserVerificationService( this.userVerificationService = new UserVerificationService(
this.stateService, this.stateService,
this.cryptoService, this.cryptoService,
@ -729,7 +741,7 @@ export default class MainBackground {
this.i18nService, this.i18nService,
this.userVerificationApiService, this.userVerificationApiService,
this.userDecryptionOptionsService, this.userDecryptionOptionsService,
this.pinCryptoService, this.pinService,
this.logService, this.logService,
this.vaultTimeoutSettingsService, this.vaultTimeoutSettingsService,
this.platformUtilsService, this.platformUtilsService,
@ -851,11 +863,13 @@ export default class MainBackground {
this.i18nService, this.i18nService,
this.collectionService, this.collectionService,
this.cryptoService, this.cryptoService,
this.pinService,
); );
this.individualVaultExportService = new IndividualVaultExportService( this.individualVaultExportService = new IndividualVaultExportService(
this.folderService, this.folderService,
this.cipherService, this.cipherService,
this.pinService,
this.cryptoService, this.cryptoService,
this.cryptoFunctionService, this.cryptoFunctionService,
this.kdfConfigService, this.kdfConfigService,
@ -864,6 +878,7 @@ export default class MainBackground {
this.organizationVaultExportService = new OrganizationVaultExportService( this.organizationVaultExportService = new OrganizationVaultExportService(
this.cipherService, this.cipherService,
this.apiService, this.apiService,
this.pinService,
this.cryptoService, this.cryptoService,
this.cryptoFunctionService, this.cryptoFunctionService,
this.collectionService, this.collectionService,
@ -914,6 +929,7 @@ export default class MainBackground {
}; };
this.systemService = new SystemService( this.systemService = new SystemService(
this.pinService,
this.messagingService, this.messagingService,
this.platformUtilsService, this.platformUtilsService,
systemUtilsServiceReloadCallback, systemUtilsServiceReloadCallback,

View File

@ -356,7 +356,7 @@ export class NativeMessagingBackground {
const masterKey = new SymmetricCryptoKey( const masterKey = new SymmetricCryptoKey(
Utils.fromB64ToArray(message.keyB64), Utils.fromB64ToArray(message.keyB64),
) as MasterKey; ) as MasterKey;
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey( const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
masterKey, masterKey,
encUserKey, encUserKey,
); );

View File

@ -5,6 +5,14 @@ import {
policyServiceFactory, policyServiceFactory,
PolicyServiceInitOptions, PolicyServiceInitOptions,
} from "../../admin-console/background/service-factories/policy-service.factory"; } from "../../admin-console/background/service-factories/policy-service.factory";
import {
accountServiceFactory,
AccountServiceInitOptions,
} from "../../auth/background/service-factories/account-service.factory";
import {
pinServiceFactory,
PinServiceInitOptions,
} from "../../auth/background/service-factories/pin-service.factory";
import { import {
tokenServiceFactory, tokenServiceFactory,
TokenServiceInitOptions, TokenServiceInitOptions,
@ -34,6 +42,8 @@ import {
type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions; type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions;
export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions & export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions &
AccountServiceInitOptions &
PinServiceInitOptions &
UserDecryptionOptionsServiceInitOptions & UserDecryptionOptionsServiceInitOptions &
CryptoServiceInitOptions & CryptoServiceInitOptions &
TokenServiceInitOptions & TokenServiceInitOptions &
@ -51,6 +61,8 @@ export function vaultTimeoutSettingsServiceFactory(
opts, opts,
async () => async () =>
new VaultTimeoutSettingsService( new VaultTimeoutSettingsService(
await accountServiceFactory(cache, opts),
await pinServiceFactory(cache, opts),
await userDecryptionOptionsServiceFactory(cache, opts), await userDecryptionOptionsServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await tokenServiceFactory(cache, opts), await tokenServiceFactory(cache, opts),

View File

@ -12,6 +12,10 @@ import {
internalMasterPasswordServiceFactory, internalMasterPasswordServiceFactory,
MasterPasswordServiceInitOptions, MasterPasswordServiceInitOptions,
} from "../../../auth/background/service-factories/master-password-service.factory"; } from "../../../auth/background/service-factories/master-password-service.factory";
import {
PinServiceInitOptions,
pinServiceFactory,
} from "../../../auth/background/service-factories/pin-service.factory";
import { import {
StateServiceInitOptions, StateServiceInitOptions,
stateServiceFactory, stateServiceFactory,
@ -45,6 +49,7 @@ import { StateProviderInitOptions, stateProviderFactory } from "./state-provider
type CryptoServiceFactoryOptions = FactoryOptions; type CryptoServiceFactoryOptions = FactoryOptions;
export type CryptoServiceInitOptions = CryptoServiceFactoryOptions & export type CryptoServiceInitOptions = CryptoServiceFactoryOptions &
PinServiceInitOptions &
MasterPasswordServiceInitOptions & MasterPasswordServiceInitOptions &
KeyGenerationServiceInitOptions & KeyGenerationServiceInitOptions &
CryptoFunctionServiceInitOptions & CryptoFunctionServiceInitOptions &
@ -67,6 +72,7 @@ export function cryptoServiceFactory(
opts, opts,
async () => async () =>
new BrowserCryptoService( new BrowserCryptoService(
await pinServiceFactory(cache, opts),
await internalMasterPasswordServiceFactory(cache, opts), await internalMasterPasswordServiceFactory(cache, opts),
await keyGenerationServiceFactory(cache, opts), await keyGenerationServiceFactory(cache, opts),
await cryptoFunctionServiceFactory(cache, opts), await cryptoFunctionServiceFactory(cache, opts),

View File

@ -1,5 +1,6 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
@ -19,6 +20,7 @@ import { UserKey } from "@bitwarden/common/types/key";
export class BrowserCryptoService extends CryptoService { export class BrowserCryptoService extends CryptoService {
constructor( constructor(
pinService: PinServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction, masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService, keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService, cryptoFunctionService: CryptoFunctionService,
@ -32,6 +34,7 @@ export class BrowserCryptoService extends CryptoService {
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
) { ) {
super( super(
pinService,
masterPasswordService, masterPasswordService,
keyGenerationService, keyGenerationService,
cryptoFunctionService, cryptoFunctionService,

View File

@ -163,6 +163,10 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
* the view is open. * the view is open.
*/ */
async isViewOpen(): Promise<boolean> { async isViewOpen(): Promise<boolean> {
if (this.isSafari()) {
// Query views on safari since chrome.runtime.sendMessage does not timeout and will hang.
return BrowserApi.isPopupOpen();
}
return Boolean(await BrowserApi.sendMessageWithResponse("checkVaultPopupHeartbeat")); return Boolean(await BrowserApi.sendMessageWithResponse("checkVaultPopupHeartbeat"));
} }

View File

@ -196,12 +196,13 @@ export const routerTransition = trigger("routerTransition", [
transition("vault-settings => sync", inSlideLeft), transition("vault-settings => sync", inSlideLeft),
transition("sync => vault-settings", outSlideRight), transition("sync => vault-settings", outSlideRight),
transition("tabs => excluded-domains", inSlideLeft),
transition("excluded-domains => tabs", outSlideRight),
transition("tabs => options", inSlideLeft), transition("tabs => options", inSlideLeft),
transition("options => tabs", outSlideRight), transition("options => tabs", outSlideRight),
// Appearance settings
transition("tabs => appearance", inSlideLeft),
transition("appearance => tabs", outSlideRight),
transition("tabs => premium", inSlideLeft), transition("tabs => premium", inSlideLeft),
transition("premium => tabs", outSlideRight), transition("premium => tabs", outSlideRight),
@ -219,6 +220,13 @@ export const routerTransition = trigger("routerTransition", [
transition("tabs => edit-send, send-type => edit-send", inSlideUp), transition("tabs => edit-send, send-type => edit-send", inSlideUp),
transition("edit-send => tabs, edit-send => send-type", outSlideDown), transition("edit-send => tabs, edit-send => send-type", outSlideDown),
// Notification settings
transition("tabs => notifications", inSlideLeft),
transition("notifications => tabs", outSlideRight),
transition("notifications => excluded-domains", inSlideLeft),
transition("excluded-domains => notifications", outSlideRight),
transition("tabs => autofill", inSlideLeft), transition("tabs => autofill", inSlideLeft),
transition("autofill => tabs", outSlideRight), transition("autofill => tabs", outSlideRight),

View File

@ -27,6 +27,8 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp
import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
import { NotifcationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { PremiumComponent } from "../billing/popup/settings/premium.component"; import { PremiumComponent } from "../billing/popup/settings/premium.component";
import BrowserPopupUtils from "../platform/popup/browser-popup-utils"; import BrowserPopupUtils from "../platform/popup/browser-popup-utils";
import { GeneratorComponent } from "../tools/popup/generator/generator.component"; import { GeneratorComponent } from "../tools/popup/generator/generator.component";
@ -46,7 +48,9 @@ import { PasswordHistoryComponent } from "../vault/popup/components/vault/passwo
import { ShareComponent } from "../vault/popup/components/vault/share.component"; import { ShareComponent } from "../vault/popup/components/vault/share.component";
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component"; import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component";
import { SyncComponent } from "../vault/popup/settings/sync.component"; import { SyncComponent } from "../vault/popup/settings/sync.component";
@ -54,7 +58,6 @@ import { VaultSettingsComponent } from "../vault/popup/settings/vault-settings.c
import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils"; import { extensionRefreshRedirect, extensionRefreshSwap } from "./extension-refresh-route-utils";
import { debounceNavigationGuard } from "./services/debounce-navigation.service"; import { debounceNavigationGuard } from "./services/debounce-navigation.service";
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
import { OptionsComponent } from "./settings/options.component"; import { OptionsComponent } from "./settings/options.component";
import { TabsV2Component } from "./tabs-v2.component"; import { TabsV2Component } from "./tabs-v2.component";
@ -254,6 +257,12 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
data: { state: "account-security" }, data: { state: "account-security" },
}, },
{
path: "notifications",
component: NotifcationsSettingsComponent,
canActivate: [AuthGuard],
data: { state: "notifications" },
},
{ {
path: "vault-settings", path: "vault-settings",
component: VaultSettingsComponent, component: VaultSettingsComponent,
@ -302,6 +311,12 @@ const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
data: { state: "options" }, data: { state: "options" },
}, },
{
path: "appearance",
component: AppearanceComponent,
canActivate: [AuthGuard],
data: { state: "appearance" },
},
{ {
path: "clone-cipher", path: "clone-cipher",
component: AddEditComponent, component: AddEditComponent,
@ -355,12 +370,11 @@ const routes: Routes = [
data: { state: "tabs_current" }, data: { state: "tabs_current" },
runGuardsAndResolvers: "always", runGuardsAndResolvers: "always",
}, },
{ ...extensionRefreshSwap(VaultFilterComponent, VaultV2Component, {
path: "vault", path: "vault",
component: VaultFilterComponent,
canActivate: [AuthGuard], canActivate: [AuthGuard],
data: { state: "tabs_vault" }, data: { state: "tabs_vault" },
}, }),
{ {
path: "generator", path: "generator",
component: GeneratorComponent, component: GeneratorComponent,

View File

@ -37,6 +37,8 @@ import { TwoFactorOptionsComponent } from "../auth/popup/two-factor-options.comp
import { TwoFactorComponent } from "../auth/popup/two-factor.component"; import { TwoFactorComponent } from "../auth/popup/two-factor.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component"; import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component"; import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { ExcludedDomainsComponent } from "../autofill/popup/settings/excluded-domains.component";
import { NotifcationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
import { PremiumComponent } from "../billing/popup/settings/premium.component"; import { PremiumComponent } from "../billing/popup/settings/premium.component";
import { HeaderComponent } from "../platform/popup/header.component"; import { HeaderComponent } from "../platform/popup/header.component";
import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component"; import { PopupFooterComponent } from "../platform/popup/layout/popup-footer.component";
@ -67,8 +69,10 @@ import { ShareComponent } from "../vault/popup/components/vault/share.component"
import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component"; import { VaultFilterComponent } from "../vault/popup/components/vault/vault-filter.component";
import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component"; import { VaultItemsComponent } from "../vault/popup/components/vault/vault-items.component";
import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component"; import { VaultSelectComponent } from "../vault/popup/components/vault/vault-select.component";
import { VaultV2Component } from "../vault/popup/components/vault/vault-v2.component";
import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component"; import { ViewCustomFieldsComponent } from "../vault/popup/components/vault/view-custom-fields.component";
import { ViewComponent } from "../vault/popup/components/vault/view.component"; import { ViewComponent } from "../vault/popup/components/vault/view.component";
import { AppearanceComponent } from "../vault/popup/settings/appearance.component";
import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component"; import { FolderAddEditComponent } from "../vault/popup/settings/folder-add-edit.component";
import { FoldersComponent } from "../vault/popup/settings/folders.component"; import { FoldersComponent } from "../vault/popup/settings/folders.component";
import { SyncComponent } from "../vault/popup/settings/sync.component"; import { SyncComponent } from "../vault/popup/settings/sync.component";
@ -79,7 +83,6 @@ import { AppComponent } from "./app.component";
import { PopOutComponent } from "./components/pop-out.component"; import { PopOutComponent } from "./components/pop-out.component";
import { UserVerificationComponent } from "./components/user-verification.component"; import { UserVerificationComponent } from "./components/user-verification.component";
import { ServicesModule } from "./services/services.module"; import { ServicesModule } from "./services/services.module";
import { ExcludedDomainsComponent } from "./settings/excluded-domains.component";
import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component"; import { HelpAndFeedbackComponent } from "./settings/help-and-feedback.component";
import { OptionsComponent } from "./settings/options.component"; import { OptionsComponent } from "./settings/options.component";
import { TabsV2Component } from "./tabs-v2.component"; import { TabsV2Component } from "./tabs-v2.component";
@ -147,6 +150,8 @@ import "../platform/popup/locales";
LoginViaAuthRequestComponent, LoginViaAuthRequestComponent,
LoginDecryptionOptionsComponent, LoginDecryptionOptionsComponent,
OptionsComponent, OptionsComponent,
NotifcationsSettingsComponent,
AppearanceComponent,
GeneratorComponent, GeneratorComponent,
PasswordGeneratorHistoryComponent, PasswordGeneratorHistoryComponent,
PasswordHistoryComponent, PasswordHistoryComponent,
@ -181,6 +186,7 @@ import "../platform/popup/locales";
EnvironmentSelectorComponent, EnvironmentSelectorComponent,
CurrentAccountComponent, CurrentAccountComponent,
AccountSwitcherComponent, AccountSwitcherComponent,
VaultV2Component,
], ],
providers: [CurrencyPipe, DatePipe], providers: [CurrencyPipe, DatePipe],
bootstrap: [AppComponent], bootstrap: [AppComponent],

View File

@ -424,6 +424,10 @@ img,
.modal-title, .modal-title,
.overlay-container { .overlay-container {
user-select: none; user-select: none;
&.user-select {
user-select: auto;
}
} }
app-about .modal-body > *, app-about .modal-body > *,

View File

@ -16,7 +16,7 @@ import {
CLIENT_TYPE, CLIENT_TYPE,
} from "@bitwarden/angular/services/injection-tokens"; } from "@bitwarden/angular/services/injection-tokens";
import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module";
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { AuthRequestServiceAbstraction, PinServiceAbstraction } from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
@ -209,6 +209,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: CryptoService, provide: CryptoService,
useFactory: ( useFactory: (
pinService: PinServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction, masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService, keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService, cryptoFunctionService: CryptoFunctionService,
@ -222,6 +223,7 @@ const safeProviders: SafeProvider[] = [
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
) => { ) => {
const cryptoService = new BrowserCryptoService( const cryptoService = new BrowserCryptoService(
pinService,
masterPasswordService, masterPasswordService,
keyGenerationService, keyGenerationService,
cryptoFunctionService, cryptoFunctionService,
@ -238,6 +240,7 @@ const safeProviders: SafeProvider[] = [
return cryptoService; return cryptoService;
}, },
deps: [ deps: [
PinServiceAbstraction,
InternalMasterPasswordServiceAbstraction, InternalMasterPasswordServiceAbstraction,
KeyGenerationService, KeyGenerationService,
CryptoFunctionService, CryptoFunctionService,

View File

@ -60,67 +60,6 @@
</div> </div>
<div id="totpHelp" class="box-footer">{{ "disableAutoTotpCopyDesc" | i18n }}</div> <div id="totpHelp" class="box-footer">{{ "disableAutoTotpCopyDesc" | i18n }}</div>
</div> </div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="addlogin-notification-bar">{{ "enableAddLoginNotification" | i18n }}</label>
<input
id="addlogin-notification-bar"
type="checkbox"
aria-describedby="addlogin-notification-barHelp"
(change)="updateAddLoginNotification()"
[(ngModel)]="enableAddLoginNotification"
/>
</div>
</div>
<div id="addlogin-notification-barHelp" class="box-footer">
{{
accountSwitcherEnabled
? ("addLoginNotificationDescAlt" | i18n)
: ("addLoginNotificationDesc" | i18n)
}}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="changedpass-notification-bar">{{
"enableChangedPasswordNotification" | i18n
}}</label>
<input
id="changedpass-notification-bar"
type="checkbox"
aria-describedby="changedpass-notification-barHelp"
(change)="updateChangedPasswordNotification()"
[(ngModel)]="enableChangedPasswordNotification"
/>
</div>
</div>
<div id="changedpass-notification-barHelp" class="box-footer">
{{
accountSwitcherEnabled
? ("changedPasswordNotificationDescAlt" | i18n)
: ("changedPasswordNotificationDesc" | i18n)
}}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="use-passkeys">{{ "enableUsePasskeys" | i18n }}</label>
<input
id="use-passkeys"
type="checkbox"
aria-describedby="use-passkeysHelp"
(change)="updateEnablePasskeys()"
[(ngModel)]="enablePasskeys"
/>
</div>
</div>
<div id="use-passkeysHelp" class="box-footer">
{{ "usePasskeysDesc" | i18n }}
</div>
</div>
<div class="box"> <div class="box">
<div class="box-content"> <div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow> <div class="box-content-row box-content-row-checkbox" appBoxRow>
@ -192,56 +131,5 @@
{{ "showIdentitiesCurrentTabDesc" | i18n }} {{ "showIdentitiesCurrentTabDesc" | i18n }}
</div> </div>
</div> </div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="favicon">{{ "enableFavicon" | i18n }}</label>
<input
id="favicon"
type="checkbox"
aria-describedby="faviconHelp"
(change)="updateFavicon()"
[(ngModel)]="enableFavicon"
/>
</div>
</div>
<div id="faviconHelp" class="box-footer">
{{ accountSwitcherEnabled ? ("faviconDescAlt" | i18n) : ("faviconDesc" | i18n) }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="badge">{{ "enableBadgeCounter" | i18n }}</label>
<input
id="badge"
type="checkbox"
aria-describedby="badgeHelp"
(change)="updateBadgeCounter()"
[(ngModel)]="enableBadgeCounter"
/>
</div>
</div>
<div id="badgeHelp" class="box-footer">{{ "badgeCounterDesc" | i18n }}</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="theme">{{ "theme" | i18n }}</label>
<select
id="theme"
name="Theme"
aria-describedby="themeHelp"
[(ngModel)]="theme"
(change)="saveTheme()"
>
<option *ngFor="let o of themeOptions" [ngValue]="o.value">{{ o.name }}</option>
</select>
</div>
</div>
<div id="themeHelp" class="box-footer">
{{ accountSwitcherEnabled ? ("themeDescAlt" | i18n) : ("themeDesc" | i18n) }}
</div>
</div>
</ng-container> </ng-container>
</main> </main>

View File

@ -2,9 +2,7 @@ import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types"; import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
import { import {
UriMatchStrategy, UriMatchStrategy,
@ -12,8 +10,6 @@ import {
} from "@bitwarden/common/models/domain/domain-service"; } from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { ThemeType } from "@bitwarden/common/platform/enums";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { enableAccountSwitching } from "../../platform/flags"; import { enableAccountSwitching } from "../../platform/flags";
@ -23,47 +19,29 @@ import { enableAccountSwitching } from "../../platform/flags";
templateUrl: "options.component.html", templateUrl: "options.component.html",
}) })
export class OptionsComponent implements OnInit { export class OptionsComponent implements OnInit {
enableFavicon = false;
enableBadgeCounter = true;
enableAutoFillOnPageLoad = false; enableAutoFillOnPageLoad = false;
autoFillOnPageLoadDefault = false; autoFillOnPageLoadDefault = false;
autoFillOnPageLoadOptions: any[]; autoFillOnPageLoadOptions: any[];
enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true? enableAutoTotpCopy = false; // TODO: Does it matter if this is set to false or true?
enableContextMenuItem = false; enableContextMenuItem = false;
enableAddLoginNotification = false;
enableChangedPasswordNotification = false;
enablePasskeys = true;
showCardsCurrentTab = false; showCardsCurrentTab = false;
showIdentitiesCurrentTab = false; showIdentitiesCurrentTab = false;
showClearClipboard = true; showClearClipboard = true;
theme: ThemeType;
themeOptions: any[];
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain; defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
uriMatchOptions: any[]; uriMatchOptions: any[];
clearClipboard: ClearClipboardDelaySetting; clearClipboard: ClearClipboardDelaySetting;
clearClipboardOptions: any[]; clearClipboardOptions: any[];
showGeneral = true; showGeneral = true;
showAutofill = true;
showDisplay = true; showDisplay = true;
accountSwitcherEnabled = false; accountSwitcherEnabled = false;
constructor( constructor(
private messagingService: MessagingService, private messagingService: MessagingService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
private domainSettingsService: DomainSettingsService, private domainSettingsService: DomainSettingsService,
private badgeSettingsService: BadgeSettingsServiceAbstraction,
i18nService: I18nService, i18nService: I18nService,
private themeStateService: ThemeStateService,
private vaultSettingsService: VaultSettingsService, private vaultSettingsService: VaultSettingsService,
) { ) {
this.themeOptions = [
{ name: i18nService.t("default"), value: ThemeType.System },
{ name: i18nService.t("light"), value: ThemeType.Light },
{ name: i18nService.t("dark"), value: ThemeType.Dark },
{ name: "Nord", value: ThemeType.Nord },
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
];
this.uriMatchOptions = [ this.uriMatchOptions = [
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain }, { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
{ name: i18nService.t("host"), value: UriMatchStrategy.Host }, { name: i18nService.t("host"), value: UriMatchStrategy.Host },
@ -98,14 +76,6 @@ export class OptionsComponent implements OnInit {
this.autofillSettingsService.autofillOnPageLoadDefault$, this.autofillSettingsService.autofillOnPageLoadDefault$,
); );
this.enableAddLoginNotification = await firstValueFrom(
this.userNotificationSettingsService.enableAddedLoginPrompt$,
);
this.enableChangedPasswordNotification = await firstValueFrom(
this.userNotificationSettingsService.enableChangedPasswordPrompt$,
);
this.enableContextMenuItem = await firstValueFrom( this.enableContextMenuItem = await firstValueFrom(
this.autofillSettingsService.enableContextMenu$, this.autofillSettingsService.enableContextMenu$,
); );
@ -117,14 +87,6 @@ export class OptionsComponent implements OnInit {
this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$); this.enableAutoTotpCopy = await firstValueFrom(this.autofillSettingsService.autoCopyTotp$);
this.enableFavicon = await firstValueFrom(this.domainSettingsService.showFavicons$);
this.enableBadgeCounter = await firstValueFrom(this.badgeSettingsService.enableBadgeCounter$);
this.enablePasskeys = await firstValueFrom(this.vaultSettingsService.enablePasskeys$);
this.theme = await firstValueFrom(this.themeStateService.selectedTheme$);
const defaultUriMatch = await firstValueFrom( const defaultUriMatch = await firstValueFrom(
this.domainSettingsService.defaultUriMatchStrategy$, this.domainSettingsService.defaultUriMatchStrategy$,
); );
@ -133,22 +95,6 @@ export class OptionsComponent implements OnInit {
this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$);
} }
async updateAddLoginNotification() {
await this.userNotificationSettingsService.setEnableAddedLoginPrompt(
this.enableAddLoginNotification,
);
}
async updateChangedPasswordNotification() {
await this.userNotificationSettingsService.setEnableChangedPasswordPrompt(
this.enableChangedPasswordNotification,
);
}
async updateEnablePasskeys() {
await this.vaultSettingsService.setEnablePasskeys(this.enablePasskeys);
}
async updateContextMenuItem() { async updateContextMenuItem() {
await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem); await this.autofillSettingsService.setEnableContextMenu(this.enableContextMenuItem);
this.messagingService.send("bgUpdateContextMenu"); this.messagingService.send("bgUpdateContextMenu");
@ -166,15 +112,6 @@ export class OptionsComponent implements OnInit {
await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault); await this.autofillSettingsService.setAutofillOnPageLoadDefault(this.autoFillOnPageLoadDefault);
} }
async updateFavicon() {
await this.domainSettingsService.setShowFavicons(this.enableFavicon);
}
async updateBadgeCounter() {
await this.badgeSettingsService.setEnableBadgeCounter(this.enableBadgeCounter);
this.messagingService.send("bgUpdateContextMenu");
}
async updateShowCardsCurrentTab() { async updateShowCardsCurrentTab() {
await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab); await this.vaultSettingsService.setShowCardsCurrentTab(this.showCardsCurrentTab);
} }
@ -183,10 +120,6 @@ export class OptionsComponent implements OnInit {
await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab); await this.vaultSettingsService.setShowIdentitiesCurrentTab(this.showIdentitiesCurrentTab);
} }
async saveTheme() {
await this.themeStateService.setSelectedTheme(this.theme);
}
async saveClearClipboard() { async saveClearClipboard() {
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard); await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
} }

View File

@ -1,5 +1,9 @@
import { ImportService, ImportServiceAbstraction } from "@bitwarden/importer/core"; import { ImportService, ImportServiceAbstraction } from "@bitwarden/importer/core";
import {
pinServiceFactory,
PinServiceInitOptions,
} from "../../../auth/background/service-factories/pin-service.factory";
import { import {
cryptoServiceFactory, cryptoServiceFactory,
CryptoServiceInitOptions, CryptoServiceInitOptions,
@ -36,7 +40,8 @@ export type ImportServiceInitOptions = ImportServiceFactoryOptions &
ImportApiServiceInitOptions & ImportApiServiceInitOptions &
I18nServiceInitOptions & I18nServiceInitOptions &
CollectionServiceInitOptions & CollectionServiceInitOptions &
CryptoServiceInitOptions; CryptoServiceInitOptions &
PinServiceInitOptions;
export function importServiceFactory( export function importServiceFactory(
cache: { cache: {
@ -56,6 +61,7 @@ export function importServiceFactory(
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await collectionServiceFactory(cache, opts), await collectionServiceFactory(cache, opts),
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await pinServiceFactory(cache, opts),
), ),
); );
} }

View File

@ -5,7 +5,7 @@
<div bitDialogTitle>Bitwarden</div> <div bitDialogTitle>Bitwarden</div>
<div bitDialogContent> <div bitDialogContent>
<p>&copy; Bitwarden Inc. 2015-{{ year }}</p> <p>&copy; Bitwarden Inc. 2015-{{ year }}</p>
<p>{{ "version" | i18n }}: {{ version$ | async }}</p> <p class="user-select">{{ "version" | i18n }}: {{ version$ | async }}</p>
<ng-container *ngIf="data$ | async as data"> <ng-container *ngIf="data$ | async as data">
<p *ngIf="data.isCloud"> <p *ngIf="data.isCloud">
{{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }} {{ "serverVersion" | i18n }}: {{ data.serverConfig?.version }}
@ -16,7 +16,7 @@
<ng-container *ngIf="!data.isCloud"> <ng-container *ngIf="!data.isCloud">
<ng-container *ngIf="data.serverConfig.server"> <ng-container *ngIf="data.serverConfig.server">
<p> <p class="user-select">
{{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>: {{ "serverVersion" | i18n }} <small>({{ "thirdParty" | i18n }})</small>:
{{ data.serverConfig?.version }} {{ data.serverConfig?.version }}
<span *ngIf="!data.serverConfig.isValid()"> <span *ngIf="!data.serverConfig.isValid()">
@ -28,7 +28,7 @@
</div> </div>
</ng-container> </ng-container>
<p *ngIf="!data.serverConfig.server"> <p class="user-select" *ngIf="!data.serverConfig.server">
{{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>: {{ "serverVersion" | i18n }} <small>({{ "selfHostedServer" | i18n }})</small>:
{{ data.serverConfig?.version }} {{ data.serverConfig?.version }}
<span *ngIf="!data.serverConfig.isValid()"> <span *ngIf="!data.serverConfig.isValid()">

View File

@ -9,11 +9,11 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { ButtonModule, DialogModule } from "@bitwarden/components"; import { ButtonModule, DialogModule } from "@bitwarden/components";
@Component({ @Component({
templateUrl: "about.component.html", templateUrl: "about-dialog.component.html",
standalone: true, standalone: true,
imports: [CommonModule, JslibModule, DialogModule, ButtonModule], imports: [CommonModule, JslibModule, DialogModule, ButtonModule],
}) })
export class AboutComponent { export class AboutDialogComponent {
protected year = new Date().getFullYear(); protected year = new Date().getFullYear();
protected version$: Observable<string>; protected version$: Observable<string>;

View File

@ -30,17 +30,17 @@
<button <button
type="button" type="button"
class="box-content-row box-content-row-flex text-default" class="box-content-row box-content-row-flex text-default"
routerLink="/vault-settings" routerLink="/notifications"
> >
<div class="row-main">{{ "vault" | i18n }}</div> <div class="row-main">{{ "notifications" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i> <i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button> </button>
<button <button
type="button" type="button"
class="box-content-row box-content-row-flex text-default" class="box-content-row box-content-row-flex text-default"
routerLink="/excluded-domains" routerLink="/vault-settings"
> >
<div class="row-main">{{ "excludedDomains" | i18n }}</div> <div class="row-main">{{ "vault" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i> <i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button> </button>
</div> </div>
@ -86,6 +86,14 @@
<div class="row-main">{{ "options" | i18n }}</div> <div class="row-main">{{ "options" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i> <i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button> </button>
<button
type="button"
class="box-content-row box-content-row-flex text-default"
routerLink="/appearance"
>
<div class="row-main">{{ "appearance" | i18n }}</div>
<i class="bwi bwi-angle-right bwi-lg row-sub-icon" aria-hidden="true"></i>
</button>
<button <button
type="button" type="button"
class="box-content-row box-content-row-flex text-default" class="box-content-row box-content-row-flex text-default"

View File

@ -13,7 +13,7 @@ import { DialogService } from "@bitwarden/components";
import { BrowserApi } from "../../../platform/browser/browser-api"; import { BrowserApi } from "../../../platform/browser/browser-api";
import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils"; import BrowserPopupUtils from "../../../platform/popup/browser-popup-utils";
import { AboutComponent } from "./about/about.component"; import { AboutDialogComponent } from "./about-dialog/about-dialog.component";
const RateUrls = { const RateUrls = {
[DeviceType.ChromeExtension]: [DeviceType.ChromeExtension]:
@ -84,7 +84,7 @@ export class SettingsComponent implements OnInit {
} }
about() { about() {
this.dialogService.open(AboutComponent); this.dialogService.open(AboutDialogComponent);
} }
rate() { rate() {

View File

@ -0,0 +1 @@
<h1>Vault V2 Extension Refresh</h1>

View File

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

View File

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

View File

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

View File

@ -86,7 +86,7 @@ export class UnlockCommand {
if (passwordValid) { if (passwordValid) {
await this.masterPasswordService.setMasterKey(masterKey, userId); await this.masterPasswordService.setMasterKey(masterKey, userId);
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey);
if (await this.keyConnectorService.getConvertAccountRequired()) { if (await this.keyConnectorService.getConvertAccountRequired()) {

View File

@ -10,8 +10,8 @@ import {
AuthRequestService, AuthRequestService,
LoginStrategyService, LoginStrategyService,
LoginStrategyServiceAbstraction, LoginStrategyServiceAbstraction,
PinCryptoService, PinService,
PinCryptoServiceAbstraction, PinServiceAbstraction,
UserDecryptionOptionsService, UserDecryptionOptionsService,
} from "@bitwarden/auth/common"; } from "@bitwarden/auth/common";
import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service";
@ -208,7 +208,7 @@ export class Main {
cipherFileUploadService: CipherFileUploadService; cipherFileUploadService: CipherFileUploadService;
keyConnectorService: KeyConnectorService; keyConnectorService: KeyConnectorService;
userVerificationService: UserVerificationService; userVerificationService: UserVerificationService;
pinCryptoService: PinCryptoServiceAbstraction; pinService: PinServiceAbstraction;
stateService: StateService; stateService: StateService;
autofillSettingsService: AutofillSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction;
domainSettingsService: DomainSettingsService; domainSettingsService: DomainSettingsService;
@ -363,11 +363,29 @@ export class Main {
migrationRunner, migrationRunner,
); );
this.masterPasswordService = new MasterPasswordService(this.stateProvider); this.masterPasswordService = new MasterPasswordService(
this.stateProvider,
this.stateService,
this.keyGenerationService,
this.encryptService,
);
this.kdfConfigService = new KdfConfigService(this.stateProvider); this.kdfConfigService = new KdfConfigService(this.stateProvider);
this.pinService = new PinService(
this.accountService,
this.cryptoFunctionService,
this.encryptService,
this.kdfConfigService,
this.keyGenerationService,
this.logService,
this.masterPasswordService,
this.stateProvider,
this.stateService,
);
this.cryptoService = new CryptoService( this.cryptoService = new CryptoService(
this.pinService,
this.masterPasswordService, this.masterPasswordService,
this.keyGenerationService, this.keyGenerationService,
this.cryptoFunctionService, this.cryptoFunctionService,
@ -582,6 +600,8 @@ export class Main {
this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); this.biometricStateService = new DefaultBiometricStateService(this.stateProvider);
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
this.accountService,
this.pinService,
this.userDecryptionOptionsService, this.userDecryptionOptionsService,
this.cryptoService, this.cryptoService,
this.tokenService, this.tokenService,
@ -590,14 +610,6 @@ export class Main {
this.biometricStateService, this.biometricStateService,
); );
this.pinCryptoService = new PinCryptoService(
this.stateService,
this.cryptoService,
this.vaultTimeoutSettingsService,
this.logService,
this.kdfConfigService,
);
this.userVerificationService = new UserVerificationService( this.userVerificationService = new UserVerificationService(
this.stateService, this.stateService,
this.cryptoService, this.cryptoService,
@ -606,7 +618,7 @@ export class Main {
this.i18nService, this.i18nService,
this.userVerificationApiService, this.userVerificationApiService,
this.userDecryptionOptionsService, this.userDecryptionOptionsService,
this.pinCryptoService, this.pinService,
this.logService, this.logService,
this.vaultTimeoutSettingsService, this.vaultTimeoutSettingsService,
this.platformUtilsService, this.platformUtilsService,
@ -669,11 +681,13 @@ export class Main {
this.i18nService, this.i18nService,
this.collectionService, this.collectionService,
this.cryptoService, this.cryptoService,
this.pinService,
); );
this.individualExportService = new IndividualVaultExportService( this.individualExportService = new IndividualVaultExportService(
this.folderService, this.folderService,
this.cipherService, this.cipherService,
this.pinService,
this.cryptoService, this.cryptoService,
this.cryptoFunctionService, this.cryptoFunctionService,
this.kdfConfigService, this.kdfConfigService,
@ -682,6 +696,7 @@ export class Main {
this.organizationExportService = new OrganizationVaultExportService( this.organizationExportService = new OrganizationVaultExportService(
this.cipherService, this.cipherService,
this.apiService, this.apiService,
this.pinService,
this.cryptoService, this.cryptoService,
this.cryptoFunctionService, this.cryptoFunctionService,
this.collectionService, this.collectionService,
@ -693,6 +708,8 @@ export class Main {
this.organizationExportService, this.organizationExportService,
); );
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
this.program = new Program(this); this.program = new Program(this);
this.vaultProgram = new VaultProgram(this); this.vaultProgram = new VaultProgram(this);
@ -716,8 +733,6 @@ export class Main {
); );
this.providerApiService = new ProviderApiService(this.apiService); this.providerApiService = new ProviderApiService(this.apiService);
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.cryptoService);
} }
async run() { async run() {

View File

@ -1,5 +1,6 @@
import * as chalk from "chalk"; import * as chalk from "chalk";
import { program, Command, OptionValues } from "commander"; import { program, Command, OptionValues } from "commander";
import { firstValueFrom } from "rxjs";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
@ -63,8 +64,16 @@ export class Program {
process.env.BW_NOINTERACTION = "true"; process.env.BW_NOINTERACTION = "true";
}); });
program.on("option:session", (key) => { program.on("option:session", async (key) => {
process.env.BW_SESSION = key; process.env.BW_SESSION = key;
// once we have the session key, we can set the user key in memory
const activeAccount = await firstValueFrom(this.main.accountService.activeAccount$);
if (activeAccount) {
await this.main.userAutoUnlockKeyService.setUserKeyInMemoryIfAutoUserKeySet(
activeAccount.id,
);
}
}); });
program.on("command:*", () => { program.on("command:*", () => {

View File

@ -1,12 +1,13 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { FormBuilder } from "@angular/forms"; import { FormBuilder } from "@angular/forms";
import { BehaviorSubject, firstValueFrom, Observable, Subject } from "rxjs"; import { BehaviorSubject, Observable, Subject, firstValueFrom } from "rxjs";
import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators"; import { concatMap, debounceTime, filter, map, switchMap, takeUntil, tap } from "rxjs/operators";
import { AuthRequestServiceAbstraction } from "@bitwarden/auth/common"; import { AuthRequestServiceAbstraction, PinServiceAbstraction } from "@bitwarden/auth/common";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
@ -19,7 +20,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { ThemeType, KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { KeySuffixOptions, ThemeType } from "@bitwarden/common/platform/enums";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
@ -111,6 +112,7 @@ export class SettingsComponent implements OnInit {
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor( constructor(
private accountService: AccountService,
private policyService: PolicyService, private policyService: PolicyService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private i18nService: I18nService, private i18nService: I18nService,
@ -127,6 +129,7 @@ export class SettingsComponent implements OnInit {
private desktopSettingsService: DesktopSettingsService, private desktopSettingsService: DesktopSettingsService,
private biometricStateService: BiometricStateService, private biometricStateService: BiometricStateService,
private desktopAutofillSettingsService: DesktopAutofillSettingsService, private desktopAutofillSettingsService: DesktopAutofillSettingsService,
private pinService: PinServiceAbstraction,
private authRequestService: AuthRequestServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction,
private logService: LogService, private logService: LogService,
private nativeMessagingManifestService: NativeMessagingManifestService, private nativeMessagingManifestService: NativeMessagingManifestService,
@ -243,9 +246,10 @@ export class SettingsComponent implements OnInit {
}), }),
); );
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
// Load initial values // Load initial values
const pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet(); this.userHasPinSet = await this.pinService.isPinSet(userId);
this.userHasPinSet = pinStatus !== "DISABLED";
const initialValues = { const initialValues = {
vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(), vaultTimeout: await this.vaultTimeoutSettingsService.getVaultTimeout(),

View File

@ -59,6 +59,7 @@ import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/ge
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { PinServiceAbstraction } from "../../../../../libs/auth/src/common/abstractions";
import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service";
import { Account } from "../../models/account"; import { Account } from "../../models/account";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service"; import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
@ -183,6 +184,7 @@ const safeProviders: SafeProvider[] = [
provide: SystemServiceAbstraction, provide: SystemServiceAbstraction,
useClass: SystemService, useClass: SystemService,
deps: [ deps: [
PinServiceAbstraction,
MessagingServiceAbstraction, MessagingServiceAbstraction,
PlatformUtilsServiceAbstraction, PlatformUtilsServiceAbstraction,
RELOAD_CALLBACK, RELOAD_CALLBACK,
@ -250,6 +252,7 @@ const safeProviders: SafeProvider[] = [
provide: CryptoServiceAbstraction, provide: CryptoServiceAbstraction,
useClass: ElectronCryptoService, useClass: ElectronCryptoService,
deps: [ deps: [
PinServiceAbstraction,
InternalMasterPasswordServiceAbstraction, InternalMasterPasswordServiceAbstraction,
KeyGenerationServiceAbstraction, KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,

View File

@ -12,8 +12,16 @@
<input class="tw-font-mono" bitInput type="password" formControlName="pin" /> <input class="tw-font-mono" bitInput type="password" formControlName="pin" />
<button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button> <button type="button" bitIconButton bitSuffix bitPasswordInputToggle></button>
</bit-form-field> </bit-form-field>
<label class="tw-flex tw-items-start tw-gap-2" *ngIf="showMasterPassOnRestart"> <label
<input class="tw-mt-1" type="checkbox" bitCheckbox formControlName="masterPassOnRestart" /> class="tw-flex tw-items-start tw-gap-2"
*ngIf="showMasterPasswordOnClientRestartOption"
>
<input
class="tw-mt-1"
type="checkbox"
bitCheckbox
formControlName="requireMasterPasswordOnClientRestart"
/>
<span>{{ "lockWithMasterPassOnRestart" | i18n }}</span> <span>{{ "lockWithMasterPassOnRestart" | i18n }}</span>
</label> </label>
</div> </div>

View File

@ -6,7 +6,7 @@ import { of } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe"; import { I18nPipe } from "@bitwarden/angular/platform/pipes/i18n.pipe";
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
@ -155,8 +155,8 @@ describe("LockComponent", () => {
useValue: mock<UserVerificationService>(), useValue: mock<UserVerificationService>(),
}, },
{ {
provide: PinCryptoServiceAbstraction, provide: PinServiceAbstraction,
useValue: mock<PinCryptoServiceAbstraction>(), useValue: mock<PinServiceAbstraction>(),
}, },
{ {
provide: BiometricStateService, provide: BiometricStateService,

View File

@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom, switchMap } from "rxjs"; import { firstValueFrom, switchMap } from "rxjs";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component"; import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
@ -62,7 +62,7 @@ export class LockComponent extends BaseLockComponent {
dialogService: DialogService, dialogService: DialogService,
deviceTrustService: DeviceTrustServiceAbstraction, deviceTrustService: DeviceTrustServiceAbstraction,
userVerificationService: UserVerificationService, userVerificationService: UserVerificationService,
pinCryptoService: PinCryptoServiceAbstraction, pinService: PinServiceAbstraction,
biometricStateService: BiometricStateService, biometricStateService: BiometricStateService,
accountService: AccountService, accountService: AccountService,
authService: AuthService, authService: AuthService,
@ -88,7 +88,7 @@ export class LockComponent extends BaseLockComponent {
dialogService, dialogService,
deviceTrustService, deviceTrustService,
userVerificationService, userVerificationService,
pinCryptoService, pinService,
biometricStateService, biometricStateService,
accountService, accountService,
authService, authService,

View File

@ -1,6 +1,7 @@
import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider"; import { FakeStateProvider } from "@bitwarden/common/../spec/fake-state-provider";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service"; import { FakeMasterPasswordService } from "@bitwarden/common/auth/services/master-password/fake-master-password.service";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@ -26,6 +27,7 @@ import { ElectronCryptoService } from "./electron-crypto.service";
describe("electronCryptoService", () => { describe("electronCryptoService", () => {
let sut: ElectronCryptoService; let sut: ElectronCryptoService;
const pinService = mock<PinServiceAbstraction>();
const keyGenerationService = mock<KeyGenerationService>(); const keyGenerationService = mock<KeyGenerationService>();
const cryptoFunctionService = mock<CryptoFunctionService>(); const cryptoFunctionService = mock<CryptoFunctionService>();
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
@ -46,6 +48,7 @@ describe("electronCryptoService", () => {
stateProvider = new FakeStateProvider(accountService); stateProvider = new FakeStateProvider(accountService);
sut = new ElectronCryptoService( sut = new ElectronCryptoService(
pinService,
masterPasswordService, masterPasswordService,
keyGenerationService, keyGenerationService,
cryptoFunctionService, cryptoFunctionService,

View File

@ -1,5 +1,6 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
@ -22,6 +23,7 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key";
export class ElectronCryptoService extends CryptoService { export class ElectronCryptoService extends CryptoService {
constructor( constructor(
pinService: PinServiceAbstraction,
masterPasswordService: InternalMasterPasswordServiceAbstraction, masterPasswordService: InternalMasterPasswordServiceAbstraction,
keyGenerationService: KeyGenerationService, keyGenerationService: KeyGenerationService,
cryptoFunctionService: CryptoFunctionService, cryptoFunctionService: CryptoFunctionService,
@ -35,6 +37,7 @@ export class ElectronCryptoService extends CryptoService {
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
) { ) {
super( super(
pinService,
masterPasswordService, masterPasswordService,
keyGenerationService, keyGenerationService,
cryptoFunctionService, cryptoFunctionService,
@ -174,7 +177,10 @@ export class ElectronCryptoService extends CryptoService {
if (!encUserKey) { if (!encUserKey) {
throw new Error("No user key found during biometric migration"); throw new Error("No user key found during biometric migration");
} }
const userKey = await this.decryptUserKeyWithMasterKey(masterKey, encUserKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(
masterKey,
encUserKey,
);
// migrate // migrate
await this.storeBiometricKey(userKey, userId); await this.storeBiometricKey(userKey, userId);
await this.stateService.setCryptoMasterKeyBiometric(null, { userId }); await this.stateService.setCryptoMasterKeyBiometric(null, { userId });

View File

@ -1,6 +1,6 @@
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { concatMap, takeUntil, map, lastValueFrom } from "rxjs"; import { concatMap, takeUntil, map } from "rxjs";
import { tap } from "rxjs/operators"; import { tap } from "rxjs/operators";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
@ -16,7 +16,6 @@ import { DialogService } from "@bitwarden/components";
import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component"; import { TwoFactorDuoComponent } from "../../../auth/settings/two-factor-duo.component";
import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component"; import { TwoFactorSetupComponent as BaseTwoFactorSetupComponent } from "../../../auth/settings/two-factor-setup.component";
import { TwoFactorVerifyComponent } from "../../../auth/settings/two-factor-verify.component";
@Component({ @Component({
selector: "app-two-factor-setup", selector: "app-two-factor-setup",
@ -66,17 +65,17 @@ export class TwoFactorSetupComponent extends BaseTwoFactorSetupComponent {
async manage(type: TwoFactorProviderType) { async manage(type: TwoFactorProviderType) {
switch (type) { switch (type) {
case TwoFactorProviderType.OrganizationDuo: { case TwoFactorProviderType.OrganizationDuo: {
const twoFactorVerifyDialogRef = TwoFactorVerifyComponent.open(this.dialogService, { const result: AuthResponse<TwoFactorDuoResponse> = await this.callTwoFactorVerifyDialog(
data: { type: type, organizationId: this.organizationId }, TwoFactorProviderType.OrganizationDuo,
});
const result: AuthResponse<TwoFactorDuoResponse> = await lastValueFrom(
twoFactorVerifyDialogRef.closed,
); );
if (!result) { if (!result) {
return; return;
} }
const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent); const duoComp = await this.openModal(this.duoModalRef, TwoFactorDuoComponent);
duoComp.type = TwoFactorProviderType.OrganizationDuo;
duoComp.organizationId = this.organizationId;
duoComp.auth(result); duoComp.auth(result);
duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => { duoComp.onUpdated.pipe(takeUntil(this.destroy$)).subscribe((enabled: boolean) => {
this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo); this.updateStatus(enabled, TwoFactorProviderType.OrganizationDuo);

View File

@ -2,7 +2,17 @@ import { DOCUMENT } from "@angular/common";
import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Component, Inject, NgZone, OnDestroy, OnInit } from "@angular/core";
import { NavigationEnd, Router } from "@angular/router"; import { NavigationEnd, Router } from "@angular/router";
import * as jq from "jquery"; import * as jq from "jquery";
import { Subject, filter, firstValueFrom, map, switchMap, takeUntil, timeout, timer } from "rxjs"; import {
Subject,
combineLatest,
filter,
firstValueFrom,
map,
switchMap,
takeUntil,
timeout,
timer,
} from "rxjs";
import { LogoutReason } from "@bitwarden/auth/common"; import { LogoutReason } from "@bitwarden/auth/common";
import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service"; import { EventUploadService } from "@bitwarden/common/abstractions/event/event-upload.service";
@ -16,6 +26,7 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service"; import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; import { PaymentMethodWarningsServiceAbstraction as PaymentMethodWarningService } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -242,8 +253,12 @@ export class AppComponent implements OnDestroy, OnInit {
new SendOptionsPolicy(), new SendOptionsPolicy(),
]); ]);
this.paymentMethodWarningsRefresh$ combineLatest([
this.configService.getFeatureFlag$(FeatureFlag.ShowPaymentMethodWarningBanners),
this.paymentMethodWarningsRefresh$,
])
.pipe( .pipe(
filter(([showPaymentMethodWarningBanners]) => showPaymentMethodWarningBanners),
switchMap(() => this.organizationService.memberOrganizations$), switchMap(() => this.organizationService.memberOrganizations$),
switchMap( switchMap(
async (organizations) => async (organizations) =>

View File

@ -6,6 +6,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
@ -50,6 +51,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
private userVerificationService: UserVerificationService, private userVerificationService: UserVerificationService,
private keyRotationService: UserKeyRotationService, private keyRotationService: UserKeyRotationService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) { ) {
super( super(
i18nService, i18nService,
@ -61,6 +63,7 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
stateService, stateService,
dialogService, dialogService,
kdfConfigService, kdfConfigService,
masterPasswordService,
); );
} }
@ -165,7 +168,22 @@ export class ChangePasswordComponent extends BaseChangePasswordComponent {
newMasterKey: MasterKey, newMasterKey: MasterKey,
newUserKey: [UserKey, EncString], newUserKey: [UserKey, EncString],
) { ) {
const masterKey = await this.cryptoService.getOrDeriveMasterKey(this.currentMasterPassword); const masterKey = await this.cryptoService.makeMasterKey(
this.currentMasterPassword,
await this.stateService.getEmail(),
await this.kdfConfigService.getKdfConfig(),
);
const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
if (userKey == null) {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("invalidMasterPassword"),
);
return;
}
const request = new PasswordRequest(); const request = new PasswordRequest();
request.masterPasswordHash = await this.cryptoService.hashMasterKey( request.masterPasswordHash = await this.cryptoService.hashMasterKey(
this.currentMasterPassword, this.currentMasterPassword,

View File

@ -6,6 +6,7 @@ import { takeUntil } from "rxjs";
import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/components/change-password.component";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -60,6 +61,7 @@ export class EmergencyAccessTakeoverComponent
dialogService: DialogService, dialogService: DialogService,
private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>, private dialogRef: DialogRef<EmergencyAccessTakeoverResultType>,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) { ) {
super( super(
i18nService, i18nService,
@ -71,6 +73,7 @@ export class EmergencyAccessTakeoverComponent
stateService, stateService,
dialogService, dialogService,
kdfConfigService, kdfConfigService,
masterPasswordService,
); );
} }

View File

@ -5,6 +5,7 @@ import { UpdatePasswordComponent as BaseUpdatePasswordComponent } from "@bitward
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -34,6 +35,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
userVerificationService: UserVerificationService, userVerificationService: UserVerificationService,
dialogService: DialogService, dialogService: DialogService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) { ) {
super( super(
router, router,
@ -49,6 +51,7 @@ export class UpdatePasswordComponent extends BaseUpdatePasswordComponent {
logService, logService,
dialogService, dialogService,
kdfConfigService, kdfConfigService,
masterPasswordService,
); );
} }
} }

View File

@ -1,129 +1,143 @@
<div *ngIf="selfHosted" class="page-header"> <bit-section>
<h1>{{ "subscription" | i18n }}</h1> <h2 *ngIf="!selfHosted" bitTypography="h2">{{ "goPremium" | i18n }}</h2>
</div> <bit-callout
<div *ngIf="!selfHosted" class="tabbed-header"> type="info"
<h1>{{ "goPremium" | i18n }}</h1> *ngIf="canAccessPremium$ | async"
</div> title="{{ 'youHavePremiumAccess' | i18n }}"
<bit-callout icon="bwi bwi-star-f"
type="info"
*ngIf="canAccessPremium$ | async"
title="{{ 'youHavePremiumAccess' | i18n }}"
icon="bwi bwi-star-f"
>
{{ "alreadyPremiumFromOrg" | i18n }}
</bit-callout>
<bit-callout type="success">
<p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
<ul class="bwi-ul">
<li>
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpStorage" | i18n }}
</li>
<li>
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpTwoStepOptions" | i18n }}
</li>
<li>
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpEmergency" | i18n }}
</li>
<li>
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpReports" | i18n }}
</li>
<li>
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpTotp" | i18n }}
</li>
<li>
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpSupport" | i18n }}
</li>
<li>
<i class="bwi bwi-check text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpFuture" | i18n }}
</li>
</ul>
<p class="text-lg" [ngClass]="{ 'mb-0': !selfHosted }">
{{
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
}}
<a routerLink="/create-organization" [queryParams]="{ plan: 'families' }">{{
"bitwardenFamiliesPlan" | i18n
}}</a>
</p>
<a
bitButton
href="{{ this.cloudWebVaultUrl }}/#/settings/subscription/premium"
target="_blank"
rel="noreferrer"
buttonType="secondary"
*ngIf="selfHosted"
> >
{{ "purchasePremium" | i18n }} {{ "alreadyPremiumFromOrg" | i18n }}
</a> </bit-callout>
</bit-callout> <bit-callout type="success">
<ng-container *ngIf="selfHosted"> <p>{{ "premiumUpgradeUnlockFeatures" | i18n }}</p>
<p>{{ "uploadLicenseFilePremium" | i18n }}</p> <ul class="bwi-ul">
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate> <li>
<div class="form-group"> <i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
<label for="file">{{ "licenseFile" | i18n }}</label> {{ "premiumSignUpStorage" | i18n }}
<input type="file" id="file" class="form-control-file" name="file" required /> </li>
<small class="form-text text-muted">{{ <li>
"licenseFileDesc" | i18n: "bitwarden_premium_license.json" <i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
}}</small> {{ "premiumSignUpTwoStepOptions" | i18n }}
</div> </li>
<button type="submit" buttonType="primary" bitButton [loading]="form.loading"> <li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpEmergency" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpReports" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpTotp" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpSupport" | i18n }}
</li>
<li>
<i class="bwi bwi-check tw-text-success bwi-li" aria-hidden="true"></i>
{{ "premiumSignUpFuture" | i18n }}
</li>
</ul>
<p bitTypography="body1" [ngClass]="{ 'tw-mb-0': !selfHosted }">
{{
"premiumPriceWithFamilyPlan" | i18n: (premiumPrice | currency: "$") : familyPlanMaxUserCount
}}
<a
bitLink
linkType="primary"
routerLink="/create-organization"
[queryParams]="{ plan: 'families' }"
>{{ "bitwardenFamiliesPlan" | i18n }}</a
>
</p>
<a
bitButton
href="{{ this.cloudWebVaultUrl }}/#/settings/subscription/premium"
target="_blank"
rel="noreferrer"
buttonType="secondary"
*ngIf="selfHosted"
>
{{ "purchasePremium" | i18n }}
</a>
</bit-callout>
</bit-section>
<bit-section *ngIf="selfHosted">
<p bitTypography="body1">{{ "uploadLicenseFilePremium" | i18n }}</p>
<form [formGroup]="licenseForm" [bitSubmit]="submit">
<bit-form-field>
<bit-label>{{ "licenseFile" | i18n }}</bit-label>
<div>
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{ this.licenseFile ? this.licenseFile.name : ("noFileChosen" | i18n) }}
</div>
<input
bitInput
#fileSelector
type="file"
formControlName="file"
(change)="setSelectedFile($event)"
hidden
/>
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}</bit-hint>
</bit-form-field>
<button type="submit" buttonType="primary" bitButton bitFormButton>
{{ "submit" | i18n }} {{ "submit" | i18n }}
</button> </button>
</form> </form>
</ng-container> </bit-section>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate *ngIf="!selfHosted"> <form [formGroup]="addonForm" [bitSubmit]="submit" *ngIf="!selfHosted">
<h2 class="mt-5">{{ "addons" | i18n }}</h2> <bit-section>
<div class="row"> <h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
<div class="form-group col-6"> <div class="tw-grid tw-grid-cols-12 tw-gap-4">
<label for="additionalStorage">{{ "additionalStorageGb" | i18n }}</label> <bit-form-field class="tw-col-span-6">
<input <bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
id="additionalStorage" <input
class="form-control" bitInput
type="number" formControlName="additionalStorage"
name="AdditionalStorageGb" type="number"
[(ngModel)]="additionalStorage" step="1"
min="0" placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
max="99" />
step="1" <bit-hint>{{
placeholder="{{ 'additionalStorageGbDesc' | i18n }}" "additionalStorageIntervalDesc"
/> | i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n)
<small class="text-muted form-text">{{ }}</bit-hint>
"additionalStorageIntervalDesc" </bit-form-field>
| i18n: "1 GB" : (storageGbPrice | currency: "$") : ("year" | i18n)
}}</small>
</div> </div>
</div> </bit-section>
<h2 class="spaced-header">{{ "summary" | i18n }}</h2> <bit-section>
{{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br /> <h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
{{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB &times; {{ "premiumMembership" | i18n }}: {{ premiumPrice | currency: "$" }} <br />
{{ storageGbPrice | currency: "$" }} = {{ "additionalStorageGb" | i18n }}: {{ additionalStorage || 0 }} GB &times;
{{ additionalStorageTotal | currency: "$" }} {{ storageGbPrice | currency: "$" }} =
<hr class="my-3" /> {{ additionalStorageTotal | currency: "$" }}
<h2 class="spaced-header mb-4">{{ "paymentInformation" | i18n }}</h2> <hr class="tw-my-3" />
<app-payment [hideBank]="true"></app-payment> </bit-section>
<app-tax-info></app-tax-info> <bit-section>
<div id="price" class="my-4"> <h3 bitTypography="h2">{{ "paymentInformation" | i18n }}</h3>
<div class="text-muted text-sm"> <app-payment [hideBank]="true"></app-payment>
{{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }} <app-tax-info></app-tax-info>
<br /> <div id="price" class="tw-my-4">
<ng-container> <div class="tw-text-muted tw-text-sm">
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }} {{ "planPrice" | i18n }}: {{ subtotal | currency: "USD $" }}
</ng-container> <br />
<ng-container>
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
</ng-container>
</div>
<hr class="tw-my-1 tw-w-1/4 tw-ml-0" />
<p bitTypography="body1">
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }}
</p>
</div> </div>
<hr class="my-1 col-3 ml-0" /> <p bitTypography="body2">{{ "paymentChargedAnnually" | i18n }}</p>
<p class="text-lg"> <button type="submit" bitButton bitFormButton>
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ "year" | i18n }} {{ "submit" | i18n }}
</p> </button>
</div> </bit-section>
<small class="text-muted font-italic">{{ "paymentChargedAnnually" | i18n }}</small>
<button type="submit" bitButton [loading]="form.loading">
{{ "submit" | i18n }}
</button>
</form> </form>

View File

@ -1,4 +1,5 @@
import { Component, OnInit, ViewChild } from "@angular/core"; import { Component, OnInit, ViewChild } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom, Observable } from "rxjs"; import { firstValueFrom, Observable } from "rxjs";
@ -7,7 +8,6 @@ import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@ -26,11 +26,16 @@ export class PremiumComponent implements OnInit {
premiumPrice = 10; premiumPrice = 10;
familyPlanMaxUserCount = 6; familyPlanMaxUserCount = 6;
storageGbPrice = 4; storageGbPrice = 4;
additionalStorage = 0;
cloudWebVaultUrl: string; cloudWebVaultUrl: string;
licenseFile: File = null;
formPromise: Promise<any>; formPromise: Promise<any>;
protected licenseForm = new FormGroup({
file: new FormControl(null, [Validators.required]),
});
protected addonForm = new FormGroup({
additionalStorage: new FormControl(0, [Validators.max(99), Validators.min(0)]),
});
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private i18nService: I18nService, private i18nService: I18nService,
@ -39,14 +44,17 @@ export class PremiumComponent implements OnInit {
private router: Router, private router: Router,
private messagingService: MessagingService, private messagingService: MessagingService,
private syncService: SyncService, private syncService: SyncService,
private logService: LogService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private billingAccountProfileStateService: BillingAccountProfileStateService, private billingAccountProfileStateService: BillingAccountProfileStateService,
) { ) {
this.selfHosted = platformUtilsService.isSelfHost(); this.selfHosted = platformUtilsService.isSelfHost();
this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$;
} }
protected setSelectedFile(event: Event) {
const fileInputEl = <HTMLInputElement>event.target;
const file: File = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
this.licenseFile = file;
}
async ngOnInit() { async ngOnInit() {
this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$); this.cloudWebVaultUrl = await firstValueFrom(this.environmentService.cloudWebVaultUrl$);
if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) { if (await firstValueFrom(this.billingAccountProfileStateService.hasPremiumPersonally$)) {
@ -56,13 +64,11 @@ export class PremiumComponent implements OnInit {
return; return;
} }
} }
submit = async () => {
async submit() { this.licenseForm.markAllAsTouched();
let files: FileList = null; this.addonForm.markAllAsTouched();
if (this.selfHosted) { if (this.selfHosted) {
const fileEl = document.getElementById("file") as HTMLInputElement; if (this.licenseFile == null) {
files = fileEl.files;
if (files == null || files.length === 0) {
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"error", "error",
this.i18nService.t("errorOccurred"), this.i18nService.t("errorOccurred"),
@ -72,53 +78,48 @@ export class PremiumComponent implements OnInit {
} }
} }
try { if (this.selfHosted) {
if (this.selfHosted) { // eslint-disable-next-line @typescript-eslint/no-misused-promises
// eslint-disable-next-line @typescript-eslint/no-misused-promises if (!this.tokenService.getEmailVerified()) {
if (!this.tokenService.getEmailVerified()) { this.platformUtilsService.showToast(
this.platformUtilsService.showToast( "error",
"error", this.i18nService.t("errorOccurred"),
this.i18nService.t("errorOccurred"), this.i18nService.t("verifyEmailFirst"),
this.i18nService.t("verifyEmailFirst"), );
); return;
return;
}
const fd = new FormData();
fd.append("license", files[0]);
this.formPromise = this.apiService.postAccountLicense(fd).then(() => {
return this.finalizePremium();
});
} else {
this.formPromise = this.paymentComponent
.createPaymentToken()
.then((result) => {
const fd = new FormData();
fd.append("paymentMethodType", result[1].toString());
if (result[0] != null) {
fd.append("paymentToken", result[0]);
}
fd.append("additionalStorageGb", (this.additionalStorage || 0).toString());
fd.append("country", this.taxInfoComponent.taxInfo.country);
fd.append("postalCode", this.taxInfoComponent.taxInfo.postalCode);
return this.apiService.postPremium(fd);
})
.then((paymentResponse) => {
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {
return this.paymentComponent.handleStripeCardPayment(
paymentResponse.paymentIntentClientSecret,
() => this.finalizePremium(),
);
} else {
return this.finalizePremium();
}
});
} }
await this.formPromise;
} catch (e) { const fd = new FormData();
this.logService.error(e); fd.append("license", this.licenseFile);
await this.apiService.postAccountLicense(fd).then(() => {
return this.finalizePremium();
});
} else {
await this.paymentComponent
.createPaymentToken()
.then((result) => {
const fd = new FormData();
fd.append("paymentMethodType", result[1].toString());
if (result[0] != null) {
fd.append("paymentToken", result[0]);
}
fd.append("additionalStorageGb", (this.additionalStorage || 0).toString());
fd.append("country", this.taxInfoComponent.taxInfo.country);
fd.append("postalCode", this.taxInfoComponent.taxInfo.postalCode);
return this.apiService.postPremium(fd);
})
.then((paymentResponse) => {
if (!paymentResponse.success && paymentResponse.paymentIntentClientSecret != null) {
return this.paymentComponent.handleStripeCardPayment(
paymentResponse.paymentIntentClientSecret,
() => this.finalizePremium(),
);
} else {
return this.finalizePremium();
}
});
} }
} };
async finalizePremium() { async finalizePremium() {
await this.apiService.refreshIdentityToken(); await this.apiService.refreshIdentityToken();
@ -127,6 +128,9 @@ export class PremiumComponent implements OnInit {
await this.router.navigate(["/settings/subscription/user-subscription"]); await this.router.navigate(["/settings/subscription/user-subscription"]);
} }
get additionalStorage(): number {
return this.addonForm.get("additionalStorage").value;
}
get additionalStorageTotal(): number { get additionalStorageTotal(): number {
return this.storageGbPrice * Math.abs(this.additionalStorage || 0); return this.storageGbPrice * Math.abs(this.additionalStorage || 0);
} }

View File

@ -1,65 +1,57 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate> <form [formGroup]="adjustSubscriptionForm" [bitSubmit]="submit">
<div> <div class="tw-grid tw-grid-cols-12 tw-gap-4">
<div class="row"> <div class="tw-col-span-8">
<div class="form-group col-8"> <bit-form-field>
<label for="newSeatCount">{{ "subscriptionSeats" | i18n }}</label> <bit-label>{{ "subscriptionSeats" | i18n }}</bit-label>
<input <input bitInput formControlName="newSeatCount" type="number" min="0" step="1" />
id="newSeatCount" <bit-hint>
class="form-control"
type="number"
name="NewSeatCount"
[(ngModel)]="newSeatCount"
min="0"
step="1"
required
/>
<small class="d-block text-muted mb-4">
<strong>{{ "total" | i18n }}:</strong> {{ additionalSeatCount || 0 }} &times; <strong>{{ "total" | i18n }}:</strong> {{ additionalSeatCount || 0 }} &times;
{{ seatPrice | currency: "$" }} = {{ adjustedSeatTotal | currency: "$" }} / {{ seatPrice | currency: "$" }} = {{ adjustedSeatTotal | currency: "$" }} /
{{ interval | i18n }} {{ interval | i18n }}</bit-hint
</small> >
</div> </bit-form-field>
</div> </div>
<div class="row mb-4"> </div>
<div class="form-group col-sm"> <div>
<div class="form-check"> <bit-form-control>
<input <input
id="limitSubscription" bitCheckbox
class="form-check-input" formControlName="limitSubscription"
type="checkbox" type="checkbox"
name="LimitSubscription" (change)="limitSubscriptionChanged()"
[(ngModel)]="limitSubscription" />
(change)="limitSubscriptionChanged()" <bit-label>{{ "limitSubscription" | i18n }}</bit-label>
/> <bit-hint> {{ "limitSubscriptionDesc" | i18n }}</bit-hint>
<label for="limitSubscription">{{ "limitSubscription" | i18n }}</label> </bit-form-control>
</div> </div>
<small class="d-block text-muted">{{ "limitSubscriptionDesc" | i18n }}</small> <div
</div> class="tw-grid tw-grid-cols-12 tw-gap-4 tw-mb-4"
</div> [hidden]="!adjustSubscriptionForm.value.limitSubscription"
<div class="row mb-4" [hidden]="!limitSubscription"> >
<div class="form-group col-sm"> <div class="tw-col-span-8">
<label for="maxAutoscaleSeats">{{ "maxSeatLimit" | i18n }}</label> <bit-form-field>
<bit-label>{{ "maxSeatLimit" | i18n }}</bit-label>
<input <input
id="maxAutoscaleSeats" bitInput
class="form-control col-8" formControlName="newMaxSeats"
type="number" type="number"
name="MaxAutoscaleSeats" [min]="
[(ngModel)]="newMaxSeats" adjustSubscriptionForm.value.newSeatCount == null
[min]="newSeatCount == null ? 1 : newSeatCount" ? 1
: adjustSubscriptionForm.value.newSeatCount
"
step="1" step="1"
[required]="limitSubscription"
/> />
<small class="d-block text-muted"> <bit-hint>
<strong>{{ "maxSeatCost" | i18n }}:</strong> {{ additionalMaxSeatCount || 0 }} &times; <strong>{{ "maxSeatCost" | i18n }}:</strong> {{ additionalMaxSeatCount || 0 }} &times;
{{ seatPrice | currency: "$" }} = {{ maxSeatTotal | currency: "$" }} / {{ seatPrice | currency: "$" }} = {{ maxSeatTotal | currency: "$" }} /
{{ interval | i18n }} {{ interval | i18n }}</bit-hint
</small> >
</div> </bit-form-field>
</div> </div>
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
<span>{{ "save" | i18n }}</span>
</button>
</div> </div>
<button bitButton buttonType="primary" bitFormButton type="submit">
{{ "save" | i18n }}
</button>
</form> </form>
<app-payment [showMethods]="false"></app-payment> <app-payment [showMethods]="false"></app-payment>

View File

@ -1,77 +1,102 @@
import { Component, EventEmitter, Input, Output } from "@angular/core"; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms";
import { Subject, takeUntil } from "rxjs";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction";
import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request"; import { OrganizationSubscriptionUpdateRequest } from "@bitwarden/common/billing/models/request/organization-subscription-update.request";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@Component({ @Component({
selector: "app-adjust-subscription", selector: "app-adjust-subscription",
templateUrl: "adjust-subscription.component.html", templateUrl: "adjust-subscription.component.html",
}) })
export class AdjustSubscription { export class AdjustSubscription implements OnInit, OnDestroy {
@Input() organizationId: string; @Input() organizationId: string;
@Input() maxAutoscaleSeats: number; @Input() maxAutoscaleSeats: number;
@Input() currentSeatCount: number; @Input() currentSeatCount: number;
@Input() seatPrice = 0; @Input() seatPrice = 0;
@Input() interval = "year"; @Input() interval = "year";
@Output() onAdjusted = new EventEmitter(); @Output() onAdjusted = new EventEmitter();
private destroy$ = new Subject<void>();
formPromise: Promise<void>; adjustSubscriptionForm = this.formBuilder.group({
limitSubscription: boolean; newSeatCount: [0, [Validators.min(0)]],
newSeatCount: number; limitSubscription: [false],
newMaxSeats: number; newMaxSeats: [0, [Validators.min(0)]],
});
get limitSubscription(): boolean {
return this.adjustSubscriptionForm.value.limitSubscription;
}
constructor( constructor(
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private logService: LogService,
private organizationApiService: OrganizationApiServiceAbstraction, private organizationApiService: OrganizationApiServiceAbstraction,
private formBuilder: FormBuilder,
) {} ) {}
ngOnInit() { ngOnInit() {
this.limitSubscription = this.maxAutoscaleSeats != null; this.adjustSubscriptionForm.patchValue({
this.newSeatCount = this.currentSeatCount; newSeatCount: this.currentSeatCount,
this.newMaxSeats = this.maxAutoscaleSeats; limitSubscription: this.maxAutoscaleSeats != null,
newMaxSeats: this.maxAutoscaleSeats,
});
this.adjustSubscriptionForm
.get("limitSubscription")
.valueChanges.pipe(takeUntil(this.destroy$))
.subscribe((value: boolean) => {
if (value) {
this.adjustSubscriptionForm
.get("newMaxSeats")
.addValidators([
Validators.min(
this.adjustSubscriptionForm.value.newSeatCount == null
? 1
: this.adjustSubscriptionForm.value.newSeatCount,
),
Validators.required,
]);
}
this.adjustSubscriptionForm.get("newMaxSeats").updateValueAndValidity();
});
} }
async submit() { ngOnDestroy() {
try { this.destroy$.next();
const request = new OrganizationSubscriptionUpdateRequest( this.destroy$.complete();
this.additionalSeatCount, }
this.newMaxSeats, submit = async () => {
); this.adjustSubscriptionForm.markAllAsTouched();
this.formPromise = this.organizationApiService.updatePasswordManagerSeats( if (this.adjustSubscriptionForm.invalid) {
this.organizationId, return;
request,
);
await this.formPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("subscriptionUpdated"),
);
} catch (e) {
this.logService.error(e);
} }
const request = new OrganizationSubscriptionUpdateRequest(
this.additionalSeatCount,
this.adjustSubscriptionForm.value.newMaxSeats,
);
await this.organizationApiService.updatePasswordManagerSeats(this.organizationId, request);
this.platformUtilsService.showToast("success", null, this.i18nService.t("subscriptionUpdated"));
this.onAdjusted.emit(); this.onAdjusted.emit();
} };
limitSubscriptionChanged() { limitSubscriptionChanged() {
if (!this.limitSubscription) { if (!this.adjustSubscriptionForm.value.limitSubscription) {
this.newMaxSeats = null; this.adjustSubscriptionForm.value.newMaxSeats = null;
} }
} }
get additionalSeatCount(): number { get additionalSeatCount(): number {
return this.newSeatCount ? this.newSeatCount - this.currentSeatCount : 0; return this.adjustSubscriptionForm.value.newSeatCount
? this.adjustSubscriptionForm.value.newSeatCount - this.currentSeatCount
: 0;
} }
get additionalMaxSeatCount(): number { get additionalMaxSeatCount(): number {
return this.newMaxSeats ? this.newMaxSeats - this.currentSeatCount : 0; return this.adjustSubscriptionForm.value.newMaxSeats
? this.adjustSubscriptionForm.value.newMaxSeats - this.currentSeatCount
: 0;
} }
get adjustedSeatTotal(): number { get adjustedSeatTotal(): number {

View File

@ -1,400 +1,428 @@
<ng-container *ngIf="loading"> <ng-container *ngIf="loading">
<i <i
class="bwi bwi-spinner bwi-spin text-muted" class="bwi bwi-spinner bwi-spin tw-text-muted"
title="{{ 'loading' | i18n }}" title="{{ 'loading' | i18n }}"
aria-hidden="true" aria-hidden="true"
></i> ></i>
<span class="sr-only">{{ "loading" | i18n }}</span> <span class="tw-sr-only">{{ "loading" | i18n }}</span>
</ng-container> </ng-container>
<ng-container *ngIf="createOrganization && selfHosted"> <ng-container *ngIf="createOrganization && selfHosted">
<p>{{ "uploadLicenseFileOrg" | i18n }}</p> <p bitTypography="body1">{{ "uploadLicenseFileOrg" | i18n }}</p>
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" ngNativeValidate> <form [formGroup]="selfHostedForm" [bitSubmit]="submit">
<div class="form-group"> <bit-form-field>
<label for="file">{{ "licenseFile" | i18n }}</label> <bit-label>{{ "licenseFile" | i18n }}</bit-label>
<input type="file" id="file" class="form-control-file" name="file" required /> <div>
<small class="form-text text-muted">{{ <button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
"licenseFileDesc" | i18n: "bitwarden_organization_license.json" {{ "chooseFile" | i18n }}
}}</small> </button>
</div> {{ selectedFile?.name ?? ("noFileChosen" | i18n) }}
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading"> </div>
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i> <input
<span>{{ "submit" | i18n }}</span> #fileSelector
hidden
bitInput
type="file"
formControlName="file"
(change)="setSelectedFile($event)"
accept="application/JSON"
/>
<bit-hint>{{ "licenseFileDesc" | i18n: "bitwarden_organization_license.json" }}</bit-hint>
</bit-form-field>
<button type="submit" bitButton bitFormButton buttonType="primary">
{{ "submit" | i18n }}
</button> </button>
</form> </form>
</ng-container> </ng-container>
<form <form
#form
[formGroup]="formGroup" [formGroup]="formGroup"
(ngSubmit)="submit()" [bitSubmit]="submit"
[appApiAction]="formPromise"
ngNativeValidate
*ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans" *ngIf="!loading && !selfHosted && this.passwordManagerPlans && this.secretsManagerPlans"
class="tw-pt-6" class="tw-pt-6"
> >
<app-org-info <bit-section>
(changedBusinessOwned)="changedOwnedBusiness()" <app-org-info
[formGroup]="formGroup" (changedBusinessOwned)="changedOwnedBusiness()"
[createOrganization]="createOrganization" [formGroup]="formGroup"
[isProvider]="!!providerId" [createOrganization]="createOrganization"
[acceptingSponsorship]="acceptingSponsorship" [isProvider]="!!providerId"
></app-org-info> [acceptingSponsorship]="acceptingSponsorship"
<h2 class="mt-5">{{ "chooseYourPlan" | i18n }}</h2> >
<div *ngFor="let selectableProduct of selectableProducts" class="form-check form-check-block"> </app-org-info>
<input </bit-section>
class="form-check-input" <bit-section>
type="radio" <h2 bitTypography="h2">{{ "chooseYourPlan" | i18n }}</h2>
name="product" <div *ngFor="let selectableProduct of selectableProducts">
id="product{{ selectableProduct.product }}" <bit-radio-group formControlName="product" [block]="true">
[value]="selectableProduct.product" <bit-radio-button [value]="selectableProduct.product" (change)="changedProduct()">
formControlName="product" <bit-label>{{ selectableProduct.nameLocalizationKey | i18n }}</bit-label>
(change)="changedProduct()" <bit-hint class="tw-text-sm"
/> >{{ selectableProduct.descriptionLocalizationKey | i18n: "1" }}
<label class="form-check-label" for="product{{ selectableProduct.product }}"> <ng-container
{{ selectableProduct.nameLocalizationKey | i18n }} *ngIf="selectableProduct.product === productTypes.Enterprise; else nonEnterprisePlans"
<small class="mb-1">{{ selectableProduct.descriptionLocalizationKey | i18n: "1" }}</small> >
<ng-container <ul class="tw-pl-0 tw-list-inside tw-mb-0">
*ngIf="selectableProduct.product === productTypes.Enterprise; else nonEnterprisePlans" <li>{{ "includeAllTeamsFeatures" | i18n }}</li>
> <li *ngIf="selectableProduct.hasSelfHost">{{ "onPremHostingOptional" | i18n }}</li>
<small>• {{ "includeAllTeamsFeatures" | i18n }}</small> <li *ngIf="selectableProduct.hasSso">{{ "includeSsoAuthentication" | i18n }}</li>
<small *ngIf="selectableProduct.hasSelfHost">• {{ "onPremHostingOptional" | i18n }}</small> <li *ngIf="selectableProduct.hasPolicies">
<small *ngIf="selectableProduct.hasSso">• {{ "includeSsoAuthentication" | i18n }}</small> {{ "includeEnterprisePolicies" | i18n }}
<small *ngIf="selectableProduct.hasPolicies" </li>
>• {{ "includeEnterprisePolicies" | i18n }}</small <li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
> {{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization" </li>
>• </ul>
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }} </ng-container>
</small> <ng-template #nonEnterprisePlans>
</ng-container> <ng-container
<ng-template #nonEnterprisePlans> *ngIf="selectableProduct.product === productTypes.Teams; else fullFeatureList"
<ng-container >
*ngIf="selectableProduct.product === productTypes.Teams; else fullFeatureList" <ul class="tw-pl-0 tw-list-inside tw-mb-0">
> <li>{{ "includeAllTeamsStarterFeatures" | i18n }}</li>
<small>• {{ "includeAllTeamsStarterFeatures" | i18n }}</small> <li>{{ "chooseMonthlyOrAnnualBilling" | i18n }}</li>
<small>• {{ "chooseMonthlyOrAnnualBilling" | i18n }}</small> <li>{{ "abilityToAddMoreThanNMembers" | i18n: 10 }}</li>
<small>• {{ "abilityToAddMoreThanNMembers" | i18n: 10 }}</small> <li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization"> {{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
• {{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }} </li>
</small> </ul>
</ng-container> </ng-container>
<ng-template #fullFeatureList> <ng-template #fullFeatureList>
<small *ngIf="selectableProduct.product == productTypes.Free" <ul class="tw-pl-0 tw-list-inside tw-mb-0">
>• {{ "limitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}</small <li *ngIf="selectableProduct.product == productTypes.Free">
{{ "limitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}
</li>
<li
*ngIf="
selectableProduct.product != productTypes.Free &&
selectableProduct.product != productTypes.TeamsStarter &&
selectableProduct.PasswordManager.maxSeats
"
>
{{ "addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}
</li>
<li *ngIf="!selectableProduct.PasswordManager.maxSeats">
{{ "addShareUnlimitedUsers" | i18n }}
</li>
<li *ngIf="selectableProduct.PasswordManager.maxCollections">
{{
"limitedCollections" | i18n: selectableProduct.PasswordManager.maxCollections
}}
</li>
<li *ngIf="selectableProduct.PasswordManager.maxAdditionalSeats">
{{
"addShareLimitedUsers"
| i18n: selectableProduct.PasswordManager.maxAdditionalSeats
}}
</li>
<li *ngIf="!selectableProduct.PasswordManager.maxCollections">
{{ "createUnlimitedCollections" | i18n }}
</li>
<li *ngIf="selectableProduct.PasswordManager.baseStorageGb">
{{
"gbEncryptedFileStorage"
| i18n: selectableProduct.PasswordManager.baseStorageGb + "GB"
}}
</li>
<li *ngIf="selectableProduct.hasGroups">
{{ "controlAccessWithGroups" | i18n }}
</li>
<li *ngIf="selectableProduct.hasApi">{{ "trackAuditLogs" | i18n }}</li>
<li *ngIf="selectableProduct.hasDirectory">
{{ "syncUsersFromDirectory" | i18n }}
</li>
<li *ngIf="selectableProduct.hasSelfHost">
{{ "onPremHostingOptional" | i18n }}
</li>
<li *ngIf="selectableProduct.usersGetPremium">{{ "usersGetPremium" | i18n }}</li>
<li *ngIf="selectableProduct.product != productTypes.Free">
{{ "priorityCustomerSupport" | i18n }}
</li>
<li *ngIf="selectableProduct.trialPeriodDays && createOrganization">
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
</li>
</ul>
</ng-template>
</ng-template>
</bit-hint>
</bit-radio-button>
<span *ngIf="selectableProduct.product != productTypes.Free">
<ng-container
*ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship"
> >
<small
*ngIf="
selectableProduct.product != productTypes.Free &&
selectableProduct.product != productTypes.TeamsStarter &&
selectableProduct.PasswordManager.maxSeats
"
>•
{{ "addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxSeats }}</small
>
<small *ngIf="!selectableProduct.PasswordManager.maxSeats"
>• {{ "addShareUnlimitedUsers" | i18n }}</small
>
<small *ngIf="selectableProduct.PasswordManager.maxCollections"
>•
{{
"limitedCollections" | i18n: selectableProduct.PasswordManager.maxCollections
}}</small
>
<small *ngIf="selectableProduct.PasswordManager.maxAdditionalSeats"
>•
{{
"addShareLimitedUsers" | i18n: selectableProduct.PasswordManager.maxAdditionalSeats
}}</small
>
<small *ngIf="!selectableProduct.PasswordManager.maxCollections"
>• {{ "createUnlimitedCollections" | i18n }}</small
>
<small *ngIf="selectableProduct.PasswordManager.baseStorageGb"
>•
{{
"gbEncryptedFileStorage"
| i18n: selectableProduct.PasswordManager.baseStorageGb + "GB"
}}</small
>
<small *ngIf="selectableProduct.hasGroups"
>• {{ "controlAccessWithGroups" | i18n }}</small
>
<small *ngIf="selectableProduct.hasApi">• {{ "trackAuditLogs" | i18n }}</small>
<small *ngIf="selectableProduct.hasDirectory"
>• {{ "syncUsersFromDirectory" | i18n }}</small
>
<small *ngIf="selectableProduct.hasSelfHost"
>• {{ "onPremHostingOptional" | i18n }}</small
>
<small *ngIf="selectableProduct.usersGetPremium">• {{ "usersGetPremium" | i18n }}</small>
<small *ngIf="selectableProduct.product != productTypes.Free"
>• {{ "priorityCustomerSupport" | i18n }}</small
>
<small *ngIf="selectableProduct.trialPeriodDays && createOrganization"
>•
{{ "xDayFreeTrial" | i18n: selectableProduct.trialPeriodDays }}
</small>
</ng-template>
</ng-template>
<span *ngIf="selectableProduct.product != productTypes.Free">
<ng-container *ngIf="selectableProduct.PasswordManager.basePrice && !acceptingSponsorship">
{{
(selectableProduct.isAnnual
? selectableProduct.PasswordManager.basePrice / 12
: selectableProduct.PasswordManager.basePrice
) | currency: "$"
}}
/{{ "month" | i18n }},
{{ "includesXUsers" | i18n: selectableProduct.PasswordManager.baseSeats }}
<ng-container *ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption">
{{ ("additionalUsers" | i18n).toLowerCase() }}
{{ {{
(selectableProduct.isAnnual (selectableProduct.isAnnual
? selectableProduct.PasswordManager.seatPrice / 12 ? selectableProduct.PasswordManager.basePrice / 12
: selectableProduct.PasswordManager.seatPrice : selectableProduct.PasswordManager.basePrice
) | currency: "$" ) | currency: "$"
}} }}
/{{ "month" | i18n }} /{{ "month" | i18n }},
</ng-container> {{ "includesXUsers" | i18n: selectableProduct.PasswordManager.baseSeats }}
</ng-container> <ng-container *ngIf="selectableProduct.PasswordManager.hasAdditionalSeatsOption">
</span> {{ ("additionalUsers" | i18n).toLowerCase() }}
<span {{
*ngIf=" (selectableProduct.isAnnual
!selectableProduct.PasswordManager.basePrice &&
selectableProduct.PasswordManager.hasAdditionalSeatsOption
"
>
{{
"costPerUser"
| i18n
: ((selectableProduct.isAnnual
? selectableProduct.PasswordManager.seatPrice / 12 ? selectableProduct.PasswordManager.seatPrice / 12
: selectableProduct.PasswordManager.seatPrice : selectableProduct.PasswordManager.seatPrice
) ) | currency: "$"
| currency: "$") }}
}} /{{ "month" | i18n }}
/{{ "month" | i18n }} </ng-container>
</span> </ng-container>
<span *ngIf="selectableProduct.product == productTypes.Free">{{ "freeForever" | i18n }}</span> </span>
</label> <span
</div> *ngIf="
<div *ngIf="formGroup.value.product !== productTypes.Free"> !selectableProduct.PasswordManager.basePrice &&
<ng-container selectableProduct.PasswordManager.hasAdditionalSeatsOption
"
>
{{
"costPerUser"
| i18n
: ((selectableProduct.isAnnual
? selectableProduct.PasswordManager.seatPrice / 12
: selectableProduct.PasswordManager.seatPrice
)
| currency: "$")
}}
/{{ "month" | i18n }}
</span>
<span *ngIf="selectableProduct.product == productTypes.Free">{{
"freeForever" | i18n
}}</span>
</bit-radio-group>
</div>
</bit-section>
<bit-section *ngIf="formGroup.value.product !== productTypes.Free">
<bit-section
*ngIf=" *ngIf="
selectedPlan.PasswordManager.hasAdditionalSeatsOption && selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
!selectedPlan.PasswordManager.baseSeats !selectedPlan.PasswordManager.baseSeats
" "
> >
<h2 class="mt-5">{{ "users" | i18n }}</h2> <h2 bitTypography="h2">{{ "users" | i18n }}</h2>
<div class="row"> <div class="tw-grid tw-grid-cols-12 tw-gap-4">
<div class="col-6"> <bit-form-field class="tw-col-span-6">
<label for="additionalSeats">{{ "userSeats" | i18n }}</label> <bit-label>{{ "userSeats" | i18n }}</bit-label>
<input <input
id="additionalSeats" bitInput
class="form-control"
type="number" type="number"
name="additionalSeats"
formControlName="additionalSeats" formControlName="additionalSeats"
placeholder="{{ 'userSeatsDesc' | i18n }}" placeholder="{{ 'userSeatsDesc' | i18n }}"
required required
/> />
<small class="text-muted form-text">{{ "userSeatsHowManyDesc" | i18n }}</small> <bit-hint class="tw-text-sm">{{ "userSeatsHowManyDesc" | i18n }}</bit-hint>
</div> </bit-form-field>
</div> </div>
</ng-container> </bit-section>
<h2 class="mt-5">{{ "addons" | i18n }}</h2> <bit-section>
<div <h2 bitTypography="h2">{{ "addons" | i18n }}</h2>
class="row" <div
*ngIf=" class="tw-grid tw-grid-cols-12 tw-gap-4"
selectedPlan.PasswordManager.hasAdditionalSeatsOption && *ngIf="
selectedPlan.PasswordManager.baseSeats selectedPlan.PasswordManager.hasAdditionalSeatsOption &&
" selectedPlan.PasswordManager.baseSeats
> "
<div class="form-group col-6"> >
<label for="additionalSeats">{{ "additionalUserSeats" | i18n }}</label> <bit-form-field class="tw-col-span-6">
<input <bit-label>{{ "additionalUserSeats" | i18n }}</bit-label>
id="additionalSeats"
class="form-control"
type="number"
name="additionalSeats"
formControlName="additionalSeats"
placeholder="{{ 'userSeatsDesc' | i18n }}"
/>
<small class="text-muted form-text">{{
"userSeatsAdditionalDesc"
| i18n
: selectedPlan.PasswordManager.baseSeats
: (seatPriceMonthly(selectedPlan) | currency: "$")
}}</small>
</div>
</div>
<div class="row">
<div class="form-group col-6">
<label for="additionalStorage">{{ "additionalStorageGb" | i18n }}</label>
<input
id="additionalStorage"
class="form-control"
type="number"
name="additionalStorageGb"
formControlName="additionalStorage"
step="1"
placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
/>
<small class="text-muted form-text">{{
"additionalStorageIntervalDesc"
| i18n
: "1 GB"
: (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
: ("month" | i18n)
}}</small>
</div>
</div>
<div class="row">
<div class="form-group col-6" *ngIf="selectedPlan.PasswordManager.hasPremiumAccessOption">
<div class="form-check">
<input <input
id="premiumAccess" bitInput
class="form-check-input" type="number"
type="checkbox" formControlName="additionalSeats"
name="premiumAccessAddon" placeholder="{{ 'userSeatsDesc' | i18n }}"
formControlName="premiumAccessAddon"
/> />
<label for="premiumAccess" class="form-check-label bold">{{ <bit-hint class="tx-text-sm"
"premiumAccess" | i18n >{{
}}</label> "userSeatsAdditionalDesc"
</div> | i18n
<small class="text-muted form-text">{{ : selectedPlan.PasswordManager.baseSeats
"premiumAccessDesc" | i18n: (3.33 | currency: "$") : ("month" | i18n) : (seatPriceMonthly(selectedPlan) | currency: "$")
}}</small> }}
</bit-hint>
</bit-form-field>
</div> </div>
</div> <div class="tw-grid tw-grid-cols-12 tw-gap-4">
<h2 class="spaced-header">{{ "summary" | i18n }}</h2> <bit-form-field class="tw-col-span-6">
<div class="form-check form-check-block" *ngFor="let selectablePlan of selectablePlans"> <bit-label>{{ "additionalStorageGb" | i18n }}</bit-label>
<input <input
class="form-check-input" bitInput
type="radio" type="number"
name="plan" formControlName="additionalStorage"
id="interval{{ selectablePlan.type }}" step="1"
[value]="selectablePlan.type" placeholder="{{ 'additionalStorageGbDesc' | i18n }}"
formControlName="plan" />
/> <bit-hint class="tw-text-sm">{{
<label class="form-check-label" for="interval{{ selectablePlan.type }}"> "additionalStorageIntervalDesc"
<ng-container *ngIf="selectablePlan.isAnnual"> | i18n
{{ "annually" | i18n }} : "1 GB"
<small *ngIf="selectablePlan.PasswordManager.basePrice"> : (additionalStoragePriceMonthly(selectedPlan) | currency: "$")
{{ "basePrice" | i18n }}: : ("month" | i18n)
{{ }}</bit-hint>
(selectablePlan.isAnnual </bit-form-field>
? selectablePlan.PasswordManager.basePrice / 12 </div>
: selectablePlan.PasswordManager.basePrice </bit-section>
) | currency: "$" <bit-section>
}} <div
&times; 12 class="tw-grid tw-grid-cols-12 tw-gap-4"
{{ "monthAbbr" | i18n }} *ngIf="selectedPlan.PasswordManager.hasPremiumAccessOption"
= >
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship"> <bit-form-control class="tw-col-span-6">
<span style="text-decoration: line-through">{{ <bit-label>{{ "premiumAccess" | i18n }}</bit-label>
selectablePlan.PasswordManager.basePrice | currency: "$" <input type="checkbox" bitCheckbox formControlName="premiumAccessAddon" />
}}</span> <bit-hint class="tw-text-sm">{{
{{ "freeWithSponsorship" | i18n }} "premiumAccessDesc" | i18n: (3.33 | currency: "$") : ("month" | i18n)
</ng-container> }}</bit-hint>
<ng-template #notAcceptingSponsorship> </bit-form-control>
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }} </div>
</bit-section>
<bit-section *ngFor="let selectablePlan of selectablePlans">
<h2 bitTypography="h2">{{ "summary" | i18n }}</h2>
<bit-radio-group formControlName="plan">
<bit-radio-button
type="radio"
id="interval{{ selectablePlan.type }}"
[value]="selectablePlan.type"
>
<bit-label>{{ (selectablePlan.isAnnual ? "annually" : "monthly") | i18n }}</bit-label>
<bit-hint *ngIf="selectablePlan.isAnnual">
<p
class="tw-mb-0"
bitTypography="body2"
*ngIf="selectablePlan.PasswordManager.basePrice"
>
{{ "basePrice" | i18n }}:
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.basePrice / 12
: selectablePlan.PasswordManager.basePrice
) | currency: "$"
}}
&times; 12
{{ "monthAbbr" | i18n }}
=
<ng-container *ngIf="acceptingSponsorship; else notAcceptingSponsorship">
<span class="tw-line-through">{{
selectablePlan.PasswordManager.basePrice | currency: "$"
}}</span>
{{ "freeWithSponsorship" | i18n }}
</ng-container>
<ng-template #notAcceptingSponsorship>
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
/{{ "year" | i18n }}
</ng-template>
</p>
<p
class="tw-mb-0"
bitTypography="body2"
*ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
>
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
>{{ "additionalUsers" | i18n }}:</span
>
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
{{ formGroup.controls["additionalSeats"].value || 0 }} &times;
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.seatPrice / 12
: selectablePlan.PasswordManager.seatPrice
) | currency: "$"
}}
&times; 12 {{ "monthAbbr" | i18n }} =
{{
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
| currency: "$"
}}
/{{ "year" | i18n }} /{{ "year" | i18n }}
</ng-template> </p>
</small> <p
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"> class="tw-mb-0"
<span *ngIf="selectablePlan.PasswordManager.baseSeats" bitTypography="body2"
>{{ "additionalUsers" | i18n }}:</span *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
> >
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span> {{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalSeats"].value || 0 }} &times; {{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ {{
(selectablePlan.isAnnual (selectablePlan.isAnnual
? selectablePlan.PasswordManager.seatPrice / 12 ? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
: selectablePlan.PasswordManager.seatPrice : selectablePlan.PasswordManager.additionalStoragePricePerGb
) | currency: "$" ) | currency: "$"
}} }}
&times; 12 {{ "monthAbbr" | i18n }} = &times; 12 {{ "monthAbbr" | i18n }} =
{{ {{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats) </p>
| currency: "$" </bit-hint>
}} <bit-hint *ngIf="!selectablePlan.isAnnual">
/{{ "year" | i18n }} <p
</small> class="tw-mb-0"
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"> bitTypography="body2"
{{ "additionalStorageGb" | i18n }}: *ngIf="selectablePlan.PasswordManager.basePrice"
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{
(selectablePlan.isAnnual
? selectablePlan.PasswordManager.additionalStoragePricePerGb / 12
: selectablePlan.PasswordManager.additionalStoragePricePerGb
) | currency: "$"
}}
&times; 12 {{ "monthAbbr" | i18n }} =
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "year" | i18n }}
</small>
</ng-container>
<ng-container *ngIf="!selectablePlan.isAnnual">
{{ "monthly" | i18n }}
<small *ngIf="selectablePlan.PasswordManager.basePrice">
{{ "basePrice" | i18n }}:
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
{{ "monthAbbr" | i18n }}
=
{{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
/{{ "month" | i18n }}
</small>
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption">
<span *ngIf="selectablePlan.PasswordManager.baseSeats"
>{{ "additionalUsers" | i18n }}:</span
> >
<span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span> {{ "basePrice" | i18n }}:
{{ formGroup.controls["additionalSeats"].value || 0 }} &times; {{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
{{ selectablePlan.PasswordManager.seatPrice | currency: "$" }} {{ "monthAbbr" | i18n }}
{{ "monthAbbr" | i18n }} = =
{{ {{ selectablePlan.PasswordManager.basePrice | currency: "$" }}
passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats) /{{ "month" | i18n }}
| currency: "$" </p>
}} <p
/{{ "month" | i18n }} class="tw-mb-0"
</small> bitTypography="body2"
<small *ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"> *ngIf="selectablePlan.PasswordManager.hasAdditionalSeatsOption"
{{ "additionalStorageGb" | i18n }}: >
{{ formGroup.controls["additionalStorage"].value || 0 }} &times; <span *ngIf="selectablePlan.PasswordManager.baseSeats"
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }} >{{ "additionalUsers" | i18n }}:</span
{{ "monthAbbr" | i18n }} = >
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }} <span *ngIf="!selectablePlan.PasswordManager.baseSeats">{{ "users" | i18n }}:</span>
</small> {{ formGroup.controls["additionalSeats"].value || 0 }} &times;
</ng-container> {{ selectablePlan.PasswordManager.seatPrice | currency: "$" }}
</label> {{ "monthAbbr" | i18n }} =
</div> {{
</div> passwordManagerSeatTotal(selectablePlan, formGroup.value.additionalSeats)
| currency: "$"
}}
/{{ "month" | i18n }}
</p>
<p
class="tw-mb-0"
bitTypography="body2"
*ngIf="selectablePlan.PasswordManager.hasAdditionalStorageOption"
>
{{ "additionalStorageGb" | i18n }}:
{{ formGroup.controls["additionalStorage"].value || 0 }} &times;
{{ selectablePlan.PasswordManager.additionalStoragePricePerGb | currency: "$" }}
{{ "monthAbbr" | i18n }} =
{{ additionalStorageTotal(selectablePlan) | currency: "$" }} /{{ "month" | i18n }}
</p>
</bit-hint>
</bit-radio-button>
</bit-radio-group>
</bit-section>
</bit-section>
<!-- Secrets Manager --> <!-- Secrets Manager -->
<div class="tw-my-10"> <bit-section>
<sm-subscribe <sm-subscribe
*ngIf="planOffersSecretsManager && !hasProvider" *ngIf="planOffersSecretsManager && !hasProvider"
[formGroup]="formGroup.controls.secretsManager" [formGroup]="formGroup.controls.secretsManager"
[selectedPlan]="selectedSecretsManagerPlan" [selectedPlan]="selectedSecretsManagerPlan"
[upgradeOrganization]="!createOrganization" [upgradeOrganization]="!createOrganization"
></sm-subscribe> ></sm-subscribe>
</div> </bit-section>
<!-- Payment info --> <!-- Payment info -->
<div *ngIf="formGroup.value.product !== productTypes.Free"> <bit-section *ngIf="formGroup.value.product !== productTypes.Free">
<h2 class="mb-4"> <h2 bitTypography="h2">
{{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }} {{ (createOrganization ? "paymentInformation" : "billingInformation") | i18n }}
</h2> </h2>
<small class="text-muted font-italic mb-3 d-block"> <p class="tw-text-muted tw-italic tw-mb-3 tw-block" bitTypography="body2">
{{ paymentDesc }} {{ paymentDesc }}
</small> </p>
<app-payment <app-payment
*ngIf="createOrganization || upgradeRequiresPaymentMethod" *ngIf="createOrganization || upgradeRequiresPaymentMethod"
[hideCredit]="true" [hideCredit]="true"
></app-payment> ></app-payment>
<app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info> <app-tax-info (onCountryChanged)="changedCountry()"></app-tax-info>
<div id="price" class="my-4"> <div id="price" class="tw-my-4">
<div class="text-muted text-sm"> <div class="tw-text-muted tw-text-base">
{{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }} {{ "passwordManagerPlanPrice" | i18n }}: {{ passwordManagerSubtotal | currency: "USD $" }}
<br /> <br />
<span *ngIf="planOffersSecretsManager && formGroup.value.secretsManager.enabled"> <span *ngIf="planOffersSecretsManager && formGroup.value.secretsManager.enabled">
@ -405,8 +433,8 @@
{{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }} {{ "estimatedTax" | i18n }}: {{ taxCharges | currency: "USD $" }}
</ng-container> </ng-container>
</div> </div>
<hr class="my-1 col-3 ml-0" /> <hr class="tw-my-1 tw-grid tw-grid-cols-3 tw-ml-0" />
<p class="text-lg"> <p class="tw-text-lg">
<strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{ <strong>{{ "total" | i18n }}:</strong> {{ total | currency: "USD $" }}/{{
selectedPlanInterval | i18n selectedPlanInterval | i18n
}} }}
@ -415,22 +443,29 @@
<ng-container *ngIf="!createOrganization"> <ng-container *ngIf="!createOrganization">
<app-payment [showMethods]="false"></app-payment> <app-payment [showMethods]="false"></app-payment>
</ng-container> </ng-container>
</div> </bit-section>
<div *ngIf="singleOrgPolicyBlock" class="mt-4"> <bit-section *ngIf="singleOrgPolicyBlock">
<app-callout [type]="'error'">{{ "singleOrgBlockCreateMessage" | i18n }}</app-callout> <app-callout [type]="'error'">{{ "singleOrgBlockCreateMessage" | i18n }}</app-callout>
</div> </bit-section>
<div class="mt-4"> <bit-section>
<button <button
type="submit" type="submit"
buttonType="primary" buttonType="primary"
bitButton bitButton
[loading]="form.loading" bitFormButton
[disabled]="!formGroup.valid" [disabled]="!formGroup.valid"
> >
{{ "submit" | i18n }} {{ "submit" | i18n }}
</button> </button>
<button type="button" buttonType="secondary" bitButton (click)="cancel()" *ngIf="showCancel"> <button
type="button"
buttonType="secondary"
bitButton
bitFormButton
(click)="cancel()"
*ngIf="showCancel"
>
{{ "cancel" | i18n }} {{ "cancel" | i18n }}
</button> </button>
</div> </bit-section>
</form> </form>

View File

@ -70,6 +70,7 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
@Input() showCancel = false; @Input() showCancel = false;
@Input() acceptingSponsorship = false; @Input() acceptingSponsorship = false;
@Input() currentPlan: PlanResponse; @Input() currentPlan: PlanResponse;
selectedFile: File;
@Input() @Input()
get product(): ProductType { get product(): ProductType {
@ -109,6 +110,10 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder); secretsManagerSubscription = secretsManagerSubscribeFormFactory(this.formBuilder);
selfHostedForm = this.formBuilder.group({
file: [null, [Validators.required]],
});
formGroup = this.formBuilder.group({ formGroup = this.formBuilder.group({
name: [""], name: [""],
billingEmail: ["", [Validators.email]], billingEmail: ["", [Validators.email]],
@ -527,72 +532,71 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
this.onCanceled.emit(); this.onCanceled.emit();
} }
async submit() { setSelectedFile(event: Event) {
const fileInputEl = <HTMLInputElement>event.target;
this.selectedFile = fileInputEl.files.length > 0 ? fileInputEl.files[0] : null;
}
submit = async () => {
if (this.singleOrgPolicyBlock) { if (this.singleOrgPolicyBlock) {
return; return;
} }
const doSubmit = async (): Promise<string> => {
let orgId: string = null;
if (this.createOrganization) {
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
const key = orgKey[0].encryptedString;
const collection = await this.cryptoService.encrypt(
this.i18nService.t("defaultCollection"),
orgKey[1],
);
const collectionCt = collection.encryptedString;
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
try { if (this.selfHosted) {
const doSubmit = async (): Promise<string> => { orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
let orgId: string = null;
if (this.createOrganization) {
const orgKey = await this.cryptoService.makeOrgKey<OrgKey>();
const key = orgKey[0].encryptedString;
const collection = await this.cryptoService.encrypt(
this.i18nService.t("defaultCollection"),
orgKey[1],
);
const collectionCt = collection.encryptedString;
const orgKeys = await this.cryptoService.makeKeyPair(orgKey[1]);
if (this.selfHosted) {
orgId = await this.createSelfHosted(key, collectionCt, orgKeys);
} else {
orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
}
this.platformUtilsService.showToast(
"success",
this.i18nService.t("organizationCreated"),
this.i18nService.t("organizationReadyToGo"),
);
} else { } else {
orgId = await this.updateOrganization(orgId); orgId = await this.createCloudHosted(key, collectionCt, orgKeys, orgKey[1]);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("organizationUpgraded"),
);
} }
await this.apiService.refreshIdentityToken(); this.platformUtilsService.showToast(
await this.syncService.fullSync(true); "success",
this.i18nService.t("organizationCreated"),
this.i18nService.t("organizationReadyToGo"),
);
} else {
orgId = await this.updateOrganization(orgId);
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("organizationUpgraded"),
);
}
if (!this.acceptingSponsorship && !this.isInTrialFlow) { await this.apiService.refreshIdentityToken();
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await this.syncService.fullSync(true);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/organizations/" + orgId]);
}
if (this.isInTrialFlow) { if (!this.acceptingSponsorship && !this.isInTrialFlow) {
this.onTrialBillingSuccess.emit({ // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
orgId: orgId, // eslint-disable-next-line @typescript-eslint/no-floating-promises
subLabelText: this.billingSubLabelText(), this.router.navigate(["/organizations/" + orgId]);
}); }
}
return orgId; if (this.isInTrialFlow) {
}; this.onTrialBillingSuccess.emit({
orgId: orgId,
subLabelText: this.billingSubLabelText(),
});
}
this.formPromise = doSubmit(); return orgId;
const organizationId = await this.formPromise; };
this.onSuccess.emit({ organizationId: organizationId });
// TODO: No one actually listening to this message? this.formPromise = doSubmit();
this.messagingService.send("organizationCreated", { organizationId }); const organizationId = await this.formPromise;
} catch (e) { this.onSuccess.emit({ organizationId: organizationId });
this.logService.error(e); this.messagingService.send("organizationCreated", organizationId);
} };
}
private async updateOrganization(orgId: string) { private async updateOrganization(orgId: string) {
const request = new OrganizationUpgradeRequest(); const request = new OrganizationUpgradeRequest();
@ -693,14 +697,12 @@ export class OrganizationPlansComponent implements OnInit, OnDestroy {
} }
private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) { private async createSelfHosted(key: string, collectionCt: string, orgKeys: [string, EncString]) {
const fileEl = document.getElementById("file") as HTMLInputElement; if (!this.selectedFile) {
const files = fileEl.files;
if (files == null || files.length === 0) {
throw new Error(this.i18nService.t("selectFile")); throw new Error(this.i18nService.t("selectFile"));
} }
const fd = new FormData(); const fd = new FormData();
fd.append("license", files[0]); fd.append("license", this.selectedFile);
fd.append("key", key); fd.append("key", key);
fd.append("collectionName", collectionCt); fd.append("collectionName", collectionCt);
const response = await this.organizationApiService.createLicense(fd); const response = await this.organizationApiService.createLicense(fd);

View File

@ -62,7 +62,7 @@
{{ "addOrganization" | i18n }} {{ "addOrganization" | i18n }}
</button> </button>
<button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed"> <button bitButton buttonType="secondary" type="button" [bitDialogClose]="ResultType.Closed">
{{ "close" | i18n }} {{ "cancel" | i18n }}
</button> </button>
</ng-container> </ng-container>
</bit-dialog> </bit-dialog>

View File

@ -4,6 +4,7 @@ import { Subject, takeUntil } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config"; import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@ -45,6 +46,7 @@ export class ChangePasswordComponent implements OnInit, OnDestroy {
protected stateService: StateService, protected stateService: StateService,
protected dialogService: DialogService, protected dialogService: DialogService,
protected kdfConfigService: KdfConfigService, protected kdfConfigService: KdfConfigService,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
) {} ) {}
async ngOnInit() { async ngOnInit() {

View File

@ -3,7 +3,7 @@ import { Router } from "@angular/router";
import { firstValueFrom, Subject } from "rxjs"; import { firstValueFrom, Subject } from "rxjs";
import { concatMap, map, take, takeUntil } from "rxjs/operators"; import { concatMap, map, take, takeUntil } from "rxjs/operators";
import { PinCryptoServiceAbstraction } from "@bitwarden/auth/common"; import { PinServiceAbstraction, PinLockType } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service"; import { VaultTimeoutService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout.service";
@ -30,7 +30,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service"; import { BiometricStateService } from "@bitwarden/common/platform/biometrics/biometric-state.service";
import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums"; import { HashPurpose, KeySuffixOptions } from "@bitwarden/common/platform/enums";
import { PinLockType } from "@bitwarden/common/services/vault-timeout/vault-timeout-settings.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { UserKey } from "@bitwarden/common/types/key"; import { UserKey } from "@bitwarden/common/types/key";
@ -55,7 +54,7 @@ export class LockComponent implements OnInit, OnDestroy {
protected onSuccessfulSubmit: () => Promise<void>; protected onSuccessfulSubmit: () => Promise<void>;
private invalidPinAttempts = 0; private invalidPinAttempts = 0;
private pinStatus: PinLockType; private pinLockType: PinLockType;
private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined; private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined;
@ -81,7 +80,7 @@ export class LockComponent implements OnInit, OnDestroy {
protected dialogService: DialogService, protected dialogService: DialogService,
protected deviceTrustService: DeviceTrustServiceAbstraction, protected deviceTrustService: DeviceTrustServiceAbstraction,
protected userVerificationService: UserVerificationService, protected userVerificationService: UserVerificationService,
protected pinCryptoService: PinCryptoServiceAbstraction, protected pinService: PinServiceAbstraction,
protected biometricStateService: BiometricStateService, protected biometricStateService: BiometricStateService,
protected accountService: AccountService, protected accountService: AccountService,
protected authService: AuthService, protected authService: AuthService,
@ -168,7 +167,8 @@ export class LockComponent implements OnInit, OnDestroy {
const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5; const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5;
try { try {
const userKey = await this.pinCryptoService.decryptUserKeyWithPin(this.pin); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const userKey = await this.pinService.decryptUserKeyWithPin(this.pin, userId);
if (userKey) { if (userKey) {
await this.setUserKeyAndContinue(userKey); await this.setUserKeyAndContinue(userKey);
@ -272,7 +272,7 @@ export class LockComponent implements OnInit, OnDestroy {
return; return;
} }
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.masterPasswordService.setMasterKey(masterKey, userId); await this.masterPasswordService.setMasterKey(masterKey, userId);
await this.setUserKeyAndContinue(userKey, true); await this.setUserKeyAndContinue(userKey, true);
} }
@ -358,12 +358,13 @@ export class LockComponent implements OnInit, OnDestroy {
return await this.vaultTimeoutService.logOut(userId); return await this.vaultTimeoutService.logOut(userId);
} }
this.pinStatus = await this.vaultTimeoutSettingsService.isPinLockSet(); this.pinLockType = await this.pinService.getPinLockType(userId);
const ephemeralPinSet = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId);
let ephemeralPinSet = await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
ephemeralPinSet ||= await this.stateService.getDecryptedPinProtected();
this.pinEnabled = this.pinEnabled =
(this.pinStatus === "TRANSIENT" && !!ephemeralPinSet) || this.pinStatus === "PERSISTANT"; (this.pinLockType === "EPHEMERAL" && !!ephemeralPinSet) || this.pinLockType === "PERSISTENT";
this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword(); this.masterPasswordEnabled = await this.userVerificationService.hasMasterPassword();
this.supportsBiometric = await this.platformUtilsService.supportsBiometric(); this.supportsBiometric = await this.platformUtilsService.supportsBiometric();

View File

@ -52,7 +52,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
constructor( constructor(
private accountService: AccountService, private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction, masterPasswordService: InternalMasterPasswordServiceAbstraction,
i18nService: I18nService, i18nService: I18nService,
cryptoService: CryptoService, cryptoService: CryptoService,
messagingService: MessagingService, messagingService: MessagingService,
@ -82,6 +82,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent {
stateService, stateService,
dialogService, dialogService,
kdfConfigService, kdfConfigService,
masterPasswordService,
); );
} }

View File

@ -1,63 +1,66 @@
import { DialogRef } from "@angular/cdk/dialog"; import { DialogRef } from "@angular/cdk/dialog";
import { Directive, OnInit } from "@angular/core"; import { Directive, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; import { FormBuilder, Validators } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
@Directive() @Directive()
export class SetPinComponent implements OnInit { export class SetPinComponent implements OnInit {
showMasterPassOnRestart = true; showMasterPasswordOnClientRestartOption = true;
setPinForm = this.formBuilder.group({ setPinForm = this.formBuilder.group({
pin: ["", [Validators.required]], pin: ["", [Validators.required]],
masterPassOnRestart: true, requireMasterPasswordOnClientRestart: true,
}); });
constructor( constructor(
private dialogRef: DialogRef, private accountService: AccountService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
private userVerificationService: UserVerificationService, private dialogRef: DialogRef,
private stateService: StateService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private kdfConfigService: KdfConfigService, private pinService: PinServiceAbstraction,
private userVerificationService: UserVerificationService,
) {} ) {}
async ngOnInit() { async ngOnInit() {
const hasMasterPassword = await this.userVerificationService.hasMasterPassword(); const hasMasterPassword = await this.userVerificationService.hasMasterPassword();
this.setPinForm.controls.masterPassOnRestart.setValue(hasMasterPassword); this.setPinForm.controls.requireMasterPasswordOnClientRestart.setValue(hasMasterPassword);
this.showMasterPassOnRestart = hasMasterPassword; this.showMasterPasswordOnClientRestartOption = hasMasterPassword;
} }
submit = async () => { submit = async () => {
const pin = this.setPinForm.get("pin").value; const pin = this.setPinForm.get("pin").value;
const masterPassOnRestart = this.setPinForm.get("masterPassOnRestart").value; const requireMasterPasswordOnClientRestart = this.setPinForm.get(
"requireMasterPasswordOnClientRestart",
).value;
if (Utils.isNullOrWhitespace(pin)) { if (Utils.isNullOrWhitespace(pin)) {
this.dialogRef.close(false); this.dialogRef.close(false);
return; return;
} }
const pinKey = await this.cryptoService.makePinKey( const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
pin,
await this.stateService.getEmail(),
await this.kdfConfigService.getKdfConfig(),
);
const userKey = await this.cryptoService.getUserKey(); const userKey = await this.cryptoService.getUserKey();
const pinProtectedKey = await this.cryptoService.encrypt(userKey.key, pinKey);
const encPin = await this.cryptoService.encrypt(pin, userKey);
await this.stateService.setProtectedPin(encPin.encryptedString); const userKeyEncryptedPin = await this.pinService.createUserKeyEncryptedPin(pin, userKey);
await this.pinService.setUserKeyEncryptedPin(userKeyEncryptedPin, userId);
if (masterPassOnRestart) { const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(pinProtectedKey); pin,
} else { userKey,
await this.stateService.setPinKeyEncryptedUserKey(pinProtectedKey); userId,
} );
await this.pinService.storePinKeyEncryptedUserKey(
pinKeyEncryptedUserKey,
requireMasterPasswordOnClientRestart,
userId,
);
this.dialogRef.close(true); this.dialogRef.close(true);
}; };

View File

@ -5,6 +5,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service"; import { KdfConfigService } from "@bitwarden/common/auth/abstractions/kdf-config.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; import { VerificationType } from "@bitwarden/common/auth/enums/verification-type";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request"; import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
@ -46,6 +47,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
private logService: LogService, private logService: LogService,
dialogService: DialogService, dialogService: DialogService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
) { ) {
super( super(
i18nService, i18nService,
@ -57,6 +59,7 @@ export class UpdatePasswordComponent extends BaseChangePasswordComponent {
stateService, stateService,
dialogService, dialogService,
kdfConfigService, kdfConfigService,
masterPasswordService,
); );
} }

View File

@ -62,7 +62,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
dialogService: DialogService, dialogService: DialogService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
private accountService: AccountService, private accountService: AccountService,
private masterPasswordService: InternalMasterPasswordServiceAbstraction, masterPasswordService: InternalMasterPasswordServiceAbstraction,
) { ) {
super( super(
i18nService, i18nService,
@ -74,6 +74,7 @@ export class UpdateTempPasswordComponent extends BaseChangePasswordComponent {
stateService, stateService,
dialogService, dialogService,
kdfConfigService, kdfConfigService,
masterPasswordService,
); );
} }

View File

@ -4,8 +4,8 @@ import { Subject } from "rxjs";
import { import {
AuthRequestServiceAbstraction, AuthRequestServiceAbstraction,
AuthRequestService, AuthRequestService,
PinCryptoServiceAbstraction, PinServiceAbstraction,
PinCryptoService, PinService,
LoginStrategyServiceAbstraction, LoginStrategyServiceAbstraction,
LoginStrategyService, LoginStrategyService,
LoginEmailServiceAbstraction, LoginEmailServiceAbstraction,
@ -545,6 +545,7 @@ const safeProviders: SafeProvider[] = [
provide: CryptoServiceAbstraction, provide: CryptoServiceAbstraction,
useClass: CryptoService, useClass: CryptoService,
deps: [ deps: [
PinServiceAbstraction,
InternalMasterPasswordServiceAbstraction, InternalMasterPasswordServiceAbstraction,
KeyGenerationServiceAbstraction, KeyGenerationServiceAbstraction,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,
@ -661,6 +662,8 @@ const safeProviders: SafeProvider[] = [
provide: VaultTimeoutSettingsServiceAbstraction, provide: VaultTimeoutSettingsServiceAbstraction,
useClass: VaultTimeoutSettingsService, useClass: VaultTimeoutSettingsService,
deps: [ deps: [
AccountServiceAbstraction,
PinServiceAbstraction,
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
TokenServiceAbstraction, TokenServiceAbstraction,
@ -728,6 +731,7 @@ const safeProviders: SafeProvider[] = [
I18nServiceAbstraction, I18nServiceAbstraction,
CollectionServiceAbstraction, CollectionServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
PinServiceAbstraction,
], ],
}), }),
safeProvider({ safeProvider({
@ -736,6 +740,7 @@ const safeProviders: SafeProvider[] = [
deps: [ deps: [
FolderServiceAbstraction, FolderServiceAbstraction,
CipherServiceAbstraction, CipherServiceAbstraction,
PinServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,
KdfConfigServiceAbstraction, KdfConfigServiceAbstraction,
@ -747,6 +752,7 @@ const safeProviders: SafeProvider[] = [
deps: [ deps: [
CipherServiceAbstraction, CipherServiceAbstraction,
ApiServiceAbstraction, ApiServiceAbstraction,
PinServiceAbstraction,
CryptoServiceAbstraction, CryptoServiceAbstraction,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,
CollectionServiceAbstraction, CollectionServiceAbstraction,
@ -822,7 +828,7 @@ const safeProviders: SafeProvider[] = [
safeProvider({ safeProvider({
provide: InternalMasterPasswordServiceAbstraction, provide: InternalMasterPasswordServiceAbstraction,
useClass: MasterPasswordService, useClass: MasterPasswordService,
deps: [StateProvider], deps: [StateProvider, StateServiceAbstraction, KeyGenerationServiceAbstraction, EncryptService],
}), }),
safeProvider({ safeProvider({
provide: MasterPasswordServiceAbstraction, provide: MasterPasswordServiceAbstraction,
@ -855,7 +861,7 @@ const safeProviders: SafeProvider[] = [
I18nServiceAbstraction, I18nServiceAbstraction,
UserVerificationApiServiceAbstraction, UserVerificationApiServiceAbstraction,
UserDecryptionOptionsServiceAbstraction, UserDecryptionOptionsServiceAbstraction,
PinCryptoServiceAbstraction, PinServiceAbstraction,
LogService, LogService,
VaultTimeoutSettingsServiceAbstraction, VaultTimeoutSettingsServiceAbstraction,
PlatformUtilsServiceAbstraction, PlatformUtilsServiceAbstraction,
@ -1005,14 +1011,18 @@ const safeProviders: SafeProvider[] = [
], ],
}), }),
safeProvider({ safeProvider({
provide: PinCryptoServiceAbstraction, provide: PinServiceAbstraction,
useClass: PinCryptoService, useClass: PinService,
deps: [ deps: [
StateServiceAbstraction, AccountServiceAbstraction,
CryptoServiceAbstraction, CryptoFunctionServiceAbstraction,
VaultTimeoutSettingsServiceAbstraction, EncryptService,
LogService,
KdfConfigServiceAbstraction, KdfConfigServiceAbstraction,
KeyGenerationServiceAbstraction,
LogService,
MasterPasswordServiceAbstraction,
StateProvider,
StateServiceAbstraction,
], ],
}), }),
safeProvider({ safeProvider({

View File

@ -13,7 +13,7 @@
</h1> </h1>
<p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p> <p *ngIf="subtitle" bitTypography="body1">{{ subtitle }}</p>
</div> </div>
<div class="tw-mb-auto tw-mx-auto tw-grid"> <div class="tw-mb-auto tw-mx-auto tw-flex tw-flex-col tw-items-center">
<div <div
class="tw-rounded-xl tw-mb-9 tw-mx-auto sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8" class="tw-rounded-xl tw-mb-9 tw-mx-auto sm:tw-border sm:tw-border-solid sm:tw-border-secondary-300 sm:tw-p-8"
> >

View File

@ -1,4 +1,4 @@
export * from "./pin-crypto.service.abstraction"; export * from "./pin.service.abstraction";
export * from "./login-email.service"; export * from "./login-email.service";
export * from "./login-strategy.service"; export * from "./login-strategy.service";
export * from "./user-decryption-options.service.abstraction"; export * from "./user-decryption-options.service.abstraction";

View File

@ -1,5 +0,0 @@
import { UserKey } from "@bitwarden/common/types/key";
export abstract class PinCryptoServiceAbstraction {
decryptUserKeyWithPin: (pin: string) => Promise<UserKey | null>;
}

View File

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

View File

@ -127,7 +127,7 @@ describe("AuthRequestLoginStrategy", () => {
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
masterPasswordService.masterKeySubject.next(masterKey); masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId }); tokenService.decodeAccessToken.mockResolvedValue({ sub: mockUserId });
await authRequestLoginStrategy.logIn(credentials); await authRequestLoginStrategy.logIn(credentials);

View File

@ -156,7 +156,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> { private async trySetUserKeyWithMasterKey(userId: UserId): Promise<void> {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) { if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey);
} }
} }

View File

@ -262,7 +262,7 @@ describe("LoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse); apiService.postIdentityToken.mockResolvedValue(tokenResponse);
masterPasswordService.masterKeySubject.next(masterKey); masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
const result = await passwordLoginStrategy.logIn(credentials); const result = await passwordLoginStrategy.logIn(credentials);
@ -281,7 +281,7 @@ describe("LoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse); apiService.postIdentityToken.mockResolvedValue(tokenResponse);
masterPasswordService.masterKeySubject.next(masterKey); masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await passwordLoginStrategy.logIn(credentials); await passwordLoginStrategy.logIn(credentials);

View File

@ -163,7 +163,7 @@ describe("PasswordLoginStrategy", () => {
const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey; const userKey = new SymmetricCryptoKey(new Uint8Array(64).buffer as CsprngArray) as UserKey;
masterPasswordService.masterKeySubject.next(masterKey); masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
tokenService.decodeAccessToken.mockResolvedValue({ sub: userId }); tokenService.decodeAccessToken.mockResolvedValue({ sub: userId });
await passwordLoginStrategy.logIn(credentials); await passwordLoginStrategy.logIn(credentials);

View File

@ -228,7 +228,7 @@ export class PasswordLoginStrategy extends LoginStrategy {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) { if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey, userId); await this.cryptoService.setUserKey(userKey, userId);
} }
} }

View File

@ -445,11 +445,15 @@ describe("SsoLoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse); apiService.postIdentityToken.mockResolvedValue(tokenResponse);
masterPasswordService.masterKeySubject.next(masterKey); masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await ssoLoginStrategy.logIn(credentials); await ssoLoginStrategy.logIn(credentials);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey); expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
masterKey,
undefined,
undefined,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
}); });
}); });
@ -497,11 +501,15 @@ describe("SsoLoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse); apiService.postIdentityToken.mockResolvedValue(tokenResponse);
masterPasswordService.masterKeySubject.next(masterKey); masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await ssoLoginStrategy.logIn(credentials); await ssoLoginStrategy.logIn(credentials);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey); expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
masterKey,
undefined,
undefined,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey);
}); });
}); });

View File

@ -350,7 +350,7 @@ export class SsoLoginStrategy extends LoginStrategy {
return; return;
} }
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey); await this.cryptoService.setUserKey(userKey);
} }

View File

@ -190,11 +190,15 @@ describe("UserApiLoginStrategy", () => {
apiService.postIdentityToken.mockResolvedValue(tokenResponse); apiService.postIdentityToken.mockResolvedValue(tokenResponse);
masterPasswordService.masterKeySubject.next(masterKey); masterPasswordService.masterKeySubject.next(masterKey);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValue(userKey); masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(userKey);
await apiLogInStrategy.logIn(credentials); await apiLogInStrategy.logIn(credentials);
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(masterKey); expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
masterKey,
undefined,
undefined,
);
expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId); expect(cryptoService.setUserKey).toHaveBeenCalledWith(userKey, userId);
}); });
}); });

View File

@ -110,7 +110,7 @@ export class UserApiLoginStrategy extends LoginStrategy {
if (response.apiUseKeyConnector) { if (response.apiUseKeyConnector) {
const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); const masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey) { if (masterKey) {
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
await this.cryptoService.setUserKey(userKey, userId); await this.cryptoService.setUserKey(userKey, userId);
} }
} }

View File

@ -172,7 +172,9 @@ describe("AuthRequestService", () => {
masterPasswordService.masterKeySubject.next(undefined); masterPasswordService.masterKeySubject.next(undefined);
masterPasswordService.masterKeyHashSubject.next(undefined); masterPasswordService.masterKeyHashSubject.next(undefined);
cryptoService.decryptUserKeyWithMasterKey.mockResolvedValueOnce(mockDecryptedUserKey); masterPasswordService.mock.decryptUserKeyWithMasterKey.mockResolvedValue(
mockDecryptedUserKey,
);
cryptoService.setUserKey.mockResolvedValueOnce(undefined); cryptoService.setUserKey.mockResolvedValueOnce(undefined);
// Act // Act
@ -192,8 +194,10 @@ describe("AuthRequestService", () => {
mockDecryptedMasterKeyHash, mockDecryptedMasterKeyHash,
mockUserId, mockUserId,
); );
expect(cryptoService.decryptUserKeyWithMasterKey).toHaveBeenCalledWith( expect(masterPasswordService.mock.decryptUserKeyWithMasterKey).toHaveBeenCalledWith(
mockDecryptedMasterKey, mockDecryptedMasterKey,
undefined,
undefined,
); );
expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey); expect(cryptoService.setUserKey).toHaveBeenCalledWith(mockDecryptedUserKey);
}); });

View File

@ -178,7 +178,7 @@ export class AuthRequestService implements AuthRequestServiceAbstraction {
); );
// Decrypt and set user key in state // Decrypt and set user key in state
const userKey = await this.cryptoService.decryptUserKeyWithMasterKey(masterKey); const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey(masterKey);
// Set masterKey + masterKeyHash in state after decryption (in case decryption fails) // Set masterKey + masterKeyHash in state after decryption (in case decryption fails)
const userId = (await firstValueFrom(this.accountService.activeAccount$)).id; const userId = (await firstValueFrom(this.accountService.activeAccount$)).id;

View File

@ -1,4 +1,4 @@
export * from "./pin-crypto/pin-crypto.service.implementation"; export * from "./pin/pin.service.implementation";
export * from "./login-email/login-email.service"; export * from "./login-email/login-email.service";
export * from "./login-strategies/login-strategy.service"; export * from "./login-strategies/login-strategy.service";
export * from "./user-decryption-options/user-decryption-options.service"; export * from "./user-decryption-options/user-decryption-options.service";

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
import { PinLockType } from "../../services/vault-timeout/vault-timeout-settings.service";
export abstract class VaultTimeoutSettingsService { export abstract class VaultTimeoutSettingsService {
/** /**
@ -38,13 +37,6 @@ export abstract class VaultTimeoutSettingsService {
*/ */
vaultTimeoutAction$: (userId?: string) => Observable<VaultTimeoutAction>; vaultTimeoutAction$: (userId?: string) => Observable<VaultTimeoutAction>;
/**
* Has the user enabled unlock with Pin.
* @param userId The user id to check. If not provided, the current user is used
* @returns PinLockType
*/
isPinLockSet: (userId?: string) => Promise<PinLockType>;
/** /**
* Has the user enabled unlock with Biometric. * Has the user enabled unlock with Biometric.
* @param userId The user id to check. If not provided, the current user is used * @param userId The user id to check. If not provided, the current user is used

View File

@ -2,7 +2,7 @@ import { Observable } from "rxjs";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { MasterKey } from "../../types/key"; import { MasterKey, UserKey } from "../../types/key";
import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../models/domain/force-set-password-reason";
export abstract class MasterPasswordServiceAbstraction { export abstract class MasterPasswordServiceAbstraction {
@ -30,6 +30,20 @@ export abstract class MasterPasswordServiceAbstraction {
* @throws If the user ID is missing. * @throws If the user ID is missing.
*/ */
abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise<EncString>; abstract getMasterKeyEncryptedUserKey: (userId: UserId) => Promise<EncString>;
/**
* Decrypts the user key with the provided master key
* @param masterKey The user's master key
* @param userKey The user's encrypted symmetric key
* @param userId The desired user
* @throws If either the MasterKey or UserKey are not resolved, or if the UserKey encryption type
* is neither AesCbc256_B64 nor AesCbc256_HmacSha256_B64
* @returns The user key
*/
abstract decryptUserKeyWithMasterKey: (
masterKey: MasterKey,
userKey?: EncString,
userId?: string,
) => Promise<UserKey>;
} }
export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction { export abstract class InternalMasterPasswordServiceAbstraction extends MasterPasswordServiceAbstraction {

View File

@ -3,7 +3,7 @@ import { ReplaySubject, Observable } from "rxjs";
import { EncString } from "../../../platform/models/domain/enc-string"; import { EncString } from "../../../platform/models/domain/enc-string";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { MasterKey } from "../../../types/key"; import { MasterKey, UserKey } from "../../../types/key";
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
@ -61,4 +61,12 @@ export class FakeMasterPasswordService implements InternalMasterPasswordServiceA
setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> { setForceSetPasswordReason(reason: ForceSetPasswordReason, userId: UserId): Promise<void> {
return this.mock.setForceSetPasswordReason(reason, userId); return this.mock.setForceSetPasswordReason(reason, userId);
} }
decryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: EncString,
userId?: string,
): Promise<UserKey> {
return this.mock.decryptUserKeyWithMasterKey(masterKey, userKey, userId);
}
} }

View File

@ -1,5 +1,9 @@
import { firstValueFrom, map, Observable } from "rxjs"; import { firstValueFrom, map, Observable } from "rxjs";
import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { KeyGenerationService } from "../../../platform/abstractions/key-generation.service";
import { StateService } from "../../../platform/abstractions/state.service";
import { EncryptionType } from "../../../platform/enums";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { import {
@ -9,7 +13,7 @@ import {
UserKeyDefinition, UserKeyDefinition,
} from "../../../platform/state"; } from "../../../platform/state";
import { UserId } from "../../../types/guid"; import { UserId } from "../../../types/guid";
import { MasterKey } from "../../../types/key"; import { MasterKey, UserKey } from "../../../types/key";
import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction"; import { InternalMasterPasswordServiceAbstraction } from "../../abstractions/master-password.service.abstraction";
import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../models/domain/force-set-password-reason";
@ -46,7 +50,12 @@ const FORCE_SET_PASSWORD_REASON = new UserKeyDefinition<ForceSetPasswordReason>(
); );
export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction { export class MasterPasswordService implements InternalMasterPasswordServiceAbstraction {
constructor(private stateProvider: StateProvider) {} constructor(
private stateProvider: StateProvider,
private stateService: StateService,
private keyGenerationService: KeyGenerationService,
private encryptService: EncryptService,
) {}
masterKey$(userId: UserId): Observable<MasterKey> { masterKey$(userId: UserId): Observable<MasterKey> {
if (userId == null) { if (userId == null) {
@ -137,4 +146,48 @@ export class MasterPasswordService implements InternalMasterPasswordServiceAbstr
} }
await this.stateProvider.getUser(userId, FORCE_SET_PASSWORD_REASON).update((_) => reason); await this.stateProvider.getUser(userId, FORCE_SET_PASSWORD_REASON).update((_) => reason);
} }
async decryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: EncString,
userId?: UserId,
): Promise<UserKey> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
userKey ??= await this.getMasterKeyEncryptedUserKey(userId);
masterKey ??= await firstValueFrom(this.masterKey$(userId));
if (masterKey == null) {
throw new Error("No master key found.");
}
// Try one more way to get the user key if it still wasn't found.
if (userKey == null) {
const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({
userId: userId,
});
if (deprecatedKey == null) {
throw new Error("No encrypted user key found.");
}
userKey = new EncString(deprecatedKey);
}
let decUserKey: Uint8Array;
if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey);
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
const newKey = await this.keyGenerationService.stretchKey(masterKey);
decUserKey = await this.encryptService.decryptToBytes(userKey, newKey);
} else {
throw new Error("Unsupported encryption type.");
}
if (decUserKey == null) {
return null;
}
return new SymmetricCryptoKey(decUserKey) as UserKey;
}
} }

View File

@ -2,7 +2,7 @@ import { firstValueFrom } from "rxjs";
import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common";
import { PinCryptoServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin-crypto.service.abstraction"; import { PinServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin.service.abstraction";
import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service";
import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { I18nService } from "../../../platform/abstractions/i18n.service"; import { I18nService } from "../../../platform/abstractions/i18n.service";
@ -44,7 +44,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private i18nService: I18nService, private i18nService: I18nService,
private userVerificationApiService: UserVerificationApiServiceAbstraction, private userVerificationApiService: UserVerificationApiServiceAbstraction,
private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
private pinCryptoService: PinCryptoServiceAbstraction, private pinService: PinServiceAbstraction,
private logService: LogService, private logService: LogService,
private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction, private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
@ -55,10 +55,11 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
verificationType: keyof UserVerificationOptions, verificationType: keyof UserVerificationOptions,
): Promise<UserVerificationOptions> { ): Promise<UserVerificationOptions> {
if (verificationType === "client") { if (verificationType === "client") {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
const [userHasMasterPassword, pinLockType, biometricsLockSet, biometricsUserKeyStored] = const [userHasMasterPassword, pinLockType, biometricsLockSet, biometricsUserKeyStored] =
await Promise.all([ await Promise.all([
this.hasMasterPasswordAndMasterKeyHash(), this.hasMasterPasswordAndMasterKeyHash(),
this.vaultTimeoutSettingsService.isPinLockSet(), this.pinService.getPinLockType(userId),
this.vaultTimeoutSettingsService.isBiometricLockSet(), this.vaultTimeoutSettingsService.isBiometricLockSet(),
this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric), this.cryptoService.hasUserKeyStored(KeySuffixOptions.Biometric),
]); ]);
@ -137,6 +138,8 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
* @param verification User-supplied verification data (OTP, MP, PIN, or biometrics) * @param verification User-supplied verification data (OTP, MP, PIN, or biometrics)
*/ */
async verifyUser(verification: Verification): Promise<boolean> { async verifyUser(verification: Verification): Promise<boolean> {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (verificationHasSecret(verification)) { if (verificationHasSecret(verification)) {
this.validateSecretInput(verification); this.validateSecretInput(verification);
} }
@ -145,9 +148,9 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
case VerificationType.OTP: case VerificationType.OTP:
return this.verifyUserByOTP(verification); return this.verifyUserByOTP(verification);
case VerificationType.MasterPassword: case VerificationType.MasterPassword:
return this.verifyUserByMasterPassword(verification); return this.verifyUserByMasterPassword(verification, userId);
case VerificationType.PIN: case VerificationType.PIN:
return this.verifyUserByPIN(verification); return this.verifyUserByPIN(verification, userId);
case VerificationType.Biometrics: case VerificationType.Biometrics:
return this.verifyUserByBiometrics(); return this.verifyUserByBiometrics();
default: { default: {
@ -170,8 +173,12 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
private async verifyUserByMasterPassword( private async verifyUserByMasterPassword(
verification: MasterPasswordVerification, verification: MasterPasswordVerification,
userId: UserId,
): Promise<boolean> { ): Promise<boolean> {
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; if (!userId) {
throw new Error("User ID is required. Cannot verify user by master password.");
}
let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId)); let masterKey = await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (!masterKey) { if (!masterKey) {
masterKey = await this.cryptoService.makeMasterKey( masterKey = await this.cryptoService.makeMasterKey(
@ -192,8 +199,12 @@ export class UserVerificationService implements UserVerificationServiceAbstracti
return true; return true;
} }
private async verifyUserByPIN(verification: PinVerification): Promise<boolean> { private async verifyUserByPIN(verification: PinVerification, userId: UserId): Promise<boolean> {
const userKey = await this.pinCryptoService.decryptUserKeyWithPin(verification.secret); if (!userId) {
throw new Error("User ID is required. Cannot verify user by PIN.");
}
const userKey = await this.pinService.decryptUserKeyWithPin(verification.secret, userId);
return userKey != null; return userKey != null;
} }

View File

@ -16,6 +16,7 @@ export class WebAuthnLoginPrfCryptoService implements WebAuthnLoginPrfCryptoServ
return (await this.stretchKey(new Uint8Array(prf))) as PrfKey; return (await this.stretchKey(new Uint8Array(prf))) as PrfKey;
} }
// TODO: use keyGenerationService.stretchKey
private async stretchKey(key: Uint8Array): Promise<SymmetricCryptoKey> { private async stretchKey(key: Uint8Array): Promise<SymmetricCryptoKey> {
const newKey = new Uint8Array(64); const newKey = new Uint8Array(64);
const encKey = await this.cryptoFunctionService.hkdfExpand(key, "enc", 32, "sha256"); const encKey = await this.cryptoFunctionService.hkdfExpand(key, "enc", 32, "sha256");

View File

@ -5,7 +5,7 @@ import { ProfileProviderOrganizationResponse } from "../../admin-console/models/
import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response"; import { ProfileProviderResponse } from "../../admin-console/models/response/profile-provider.response";
import { KdfConfig } from "../../auth/models/domain/kdf-config"; import { KdfConfig } from "../../auth/models/domain/kdf-config";
import { OrganizationId, ProviderId, UserId } from "../../types/guid"; import { OrganizationId, ProviderId, UserId } from "../../types/guid";
import { UserKey, MasterKey, OrgKey, ProviderKey, PinKey, CipherKey } from "../../types/key"; import { UserKey, MasterKey, OrgKey, ProviderKey, CipherKey } from "../../types/key";
import { KeySuffixOptions, HashPurpose } from "../enums"; import { KeySuffixOptions, HashPurpose } from "../enums";
import { EncArrayBuffer } from "../models/domain/enc-array-buffer"; import { EncArrayBuffer } from "../models/domain/enc-array-buffer";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
@ -139,18 +139,6 @@ export abstract class CryptoService {
masterKey: MasterKey, masterKey: MasterKey,
userKey?: UserKey, userKey?: UserKey,
): Promise<[UserKey, EncString]>; ): Promise<[UserKey, EncString]>;
/**
* Decrypts the user key with the provided master key
* @param masterKey The user's master key
* @param userKey The user's encrypted symmetric key
* @param userId The desired user
* @returns The user key
*/
abstract decryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: EncString,
userId?: string,
): Promise<UserKey>;
/** /**
* Creates a master password hash from the user's master password. Can * Creates a master password hash from the user's master password. Can
* be used for local authentication or for server authentication depending * be used for local authentication or for server authentication depending
@ -268,13 +256,6 @@ export abstract class CryptoService {
* @throws If the provided key is a null-ish value. * @throws If the provided key is a null-ish value.
*/ */
abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>; abstract makeKeyPair(key: SymmetricCryptoKey): Promise<[string, EncString]>;
/**
* @param pin The user's pin
* @param salt The user's salt
* @param kdfConfig The user's kdf config
* @returns A key derived from the user's pin
*/
abstract makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey>;
/** /**
* Clears the user's pin keys from storage * Clears the user's pin keys from storage
* Note: This will remove the stored pin and as a result, * Note: This will remove the stored pin and as a result,
@ -282,39 +263,6 @@ export abstract class CryptoService {
* @param userId The desired user * @param userId The desired user
*/ */
abstract clearPinKeys(userId?: string): Promise<void>; abstract clearPinKeys(userId?: string): Promise<void>;
/**
* Decrypts the user key with their pin
* @param pin The user's PIN
* @param salt The user's salt
* @param kdfConfig The user's KDF config
* @param pinProtectedUserKey The user's PIN protected symmetric key, if not provided
* it will be retrieved from storage
* @returns The decrypted user key
*/
abstract decryptUserKeyWithPin(
pin: string,
salt: string,
kdfConfig: KdfConfig,
protectedKeyCs?: EncString,
): Promise<UserKey>;
/**
* Creates a new Pin key that encrypts the user key instead of the
* master key. Clears the old Pin key from state.
* @param masterPasswordOnRestart True if Master Password on Restart is enabled
* @param pin User's PIN
* @param email User's email
* @param kdfConfig User's KdfConfig
* @param oldPinKey The old Pin key from state (retrieved from different
* places depending on if Master Password on Restart was enabled)
* @returns The user key
*/
abstract decryptAndMigrateOldPinKey(
masterPasswordOnRestart: boolean,
pin: string,
email: string,
kdfConfig: KdfConfig,
oldPinKey: EncString,
): Promise<UserKey>;
/** /**
* @param keyMaterial The key material to derive the send key from * @param keyMaterial The key material to derive the send key from
* @returns A new send key * @returns A new send key
@ -358,16 +306,6 @@ export abstract class CryptoService {
publicKey: string; publicKey: string;
privateKey: EncString; privateKey: EncString;
}>; }>;
/**
* @deprecated Left for migration purposes. Use decryptUserKeyWithPin instead.
*/
abstract decryptMasterKeyWithPin(
pin: string,
salt: string,
kdfConfig: KdfConfig,
protectedKeyCs?: EncString,
): Promise<MasterKey>;
/** /**
* Previously, the master key was used for any additional key like the biometrics or pin key. * Previously, the master key was used for any additional key like the biometrics or pin key.
* We have switched to using the user key for these purposes. This method is for clearing the state * We have switched to using the user key for these purposes. This method is for clearing the state

View File

@ -53,4 +53,11 @@ export abstract class KeyGenerationService {
salt: string | Uint8Array, salt: string | Uint8Array,
kdfConfig: KdfConfig, kdfConfig: KdfConfig,
): Promise<SymmetricCryptoKey>; ): Promise<SymmetricCryptoKey>;
/**
* Derives a 64 byte key from a 32 byte key using a key derivation function.
* @param key 32 byte key.
* @returns 64 byte derived key.
*/
abstract stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey>;
} }

View File

@ -4,7 +4,6 @@ import { GeneratedPasswordHistory, PasswordGeneratorOptions } from "../../tools/
import { UsernameGeneratorOptions } from "../../tools/generator/username"; import { UsernameGeneratorOptions } from "../../tools/generator/username";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { Account } from "../models/domain/account"; import { Account } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
import { StorageOptions } from "../models/domain/storage-options"; import { StorageOptions } from "../models/domain/storage-options";
/** /**
@ -47,26 +46,6 @@ export abstract class StateService<T extends Account = Account> {
* Sets the user's biometric key * Sets the user's biometric key
*/ */
setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>; setUserKeyBiometric: (value: BiometricKey, options?: StorageOptions) => Promise<void>;
/**
* Gets the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is disabled
*/
getPinKeyEncryptedUserKey: (options?: StorageOptions) => Promise<EncString>;
/**
* Sets the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is disabled
*/
setPinKeyEncryptedUserKey: (value: EncString, options?: StorageOptions) => Promise<void>;
/**
* Gets the ephemeral version of the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is enabled
*/
getPinKeyEncryptedUserKeyEphemeral: (options?: StorageOptions) => Promise<EncString>;
/**
* Sets the ephemeral version of the user key encrypted by the Pin key.
* Used when Lock with MP on Restart is enabled
*/
setPinKeyEncryptedUserKeyEphemeral: (value: EncString, options?: StorageOptions) => Promise<void>;
/** /**
* @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService * @deprecated For backwards compatible purposes only, use DesktopAutofillSettingsService
*/ */
@ -101,14 +80,6 @@ export abstract class StateService<T extends Account = Account> {
value: GeneratedPasswordHistory[], value: GeneratedPasswordHistory[],
options?: StorageOptions, options?: StorageOptions,
) => Promise<void>; ) => Promise<void>;
/**
* @deprecated For migration purposes only, use getDecryptedUserKeyPin instead
*/
getDecryptedPinProtected: (options?: StorageOptions) => Promise<EncString>;
/**
* @deprecated For migration purposes only, use setDecryptedUserKeyPin instead
*/
setDecryptedPinProtected: (value: EncString, options?: StorageOptions) => Promise<void>;
getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>; getDuckDuckGoSharedKey: (options?: StorageOptions) => Promise<string>;
setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>; setDuckDuckGoSharedKey: (value: string, options?: StorageOptions) => Promise<void>;
getEmail: (options?: StorageOptions) => Promise<string>; getEmail: (options?: StorageOptions) => Promise<string>;
@ -127,14 +98,6 @@ export abstract class StateService<T extends Account = Account> {
value: GeneratedPasswordHistory[], value: GeneratedPasswordHistory[],
options?: StorageOptions, options?: StorageOptions,
) => Promise<void>; ) => Promise<void>;
/**
* @deprecated For migration purposes only, use getEncryptedUserKeyPin instead
*/
getEncryptedPinProtected: (options?: StorageOptions) => Promise<string>;
/**
* @deprecated For migration purposes only, use setEncryptedUserKeyPin instead
*/
setEncryptedPinProtected: (value: string, options?: StorageOptions) => Promise<void>;
getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>; getIsAuthenticated: (options?: StorageOptions) => Promise<boolean>;
getLastSync: (options?: StorageOptions) => Promise<string>; getLastSync: (options?: StorageOptions) => Promise<string>;
setLastSync: (value: string, options?: StorageOptions) => Promise<void>; setLastSync: (value: string, options?: StorageOptions) => Promise<void>;
@ -154,14 +117,6 @@ export abstract class StateService<T extends Account = Account> {
) => Promise<void>; ) => Promise<void>;
getGeneratorOptions: (options?: StorageOptions) => Promise<GeneratorOptions>; getGeneratorOptions: (options?: StorageOptions) => Promise<GeneratorOptions>;
setGeneratorOptions: (value: GeneratorOptions, options?: StorageOptions) => Promise<void>; setGeneratorOptions: (value: GeneratorOptions, options?: StorageOptions) => Promise<void>;
/**
* Gets the user's Pin, encrypted by the user key
*/
getProtectedPin: (options?: StorageOptions) => Promise<string>;
/**
* Sets the user's Pin, encrypted by the user key
*/
setProtectedPin: (value: string, options?: StorageOptions) => Promise<void>;
getUserId: (options?: StorageOptions) => Promise<string>; getUserId: (options?: StorageOptions) => Promise<string>;
getVaultTimeout: (options?: StorageOptions) => Promise<number>; getVaultTimeout: (options?: StorageOptions) => Promise<number>;
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>; setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;

View File

@ -1,24 +1,9 @@
import { AccountSettings, EncryptionPair } from "./account"; import { AccountSettings } from "./account";
import { EncString } from "./enc-string";
describe("AccountSettings", () => { describe("AccountSettings", () => {
describe("fromJSON", () => { describe("fromJSON", () => {
it("should deserialize to an instance of itself", () => { it("should deserialize to an instance of itself", () => {
expect(AccountSettings.fromJSON(JSON.parse("{}"))).toBeInstanceOf(AccountSettings); expect(AccountSettings.fromJSON(JSON.parse("{}"))).toBeInstanceOf(AccountSettings);
}); });
it("should deserialize pinProtected", () => {
const accountSettings = new AccountSettings();
accountSettings.pinProtected = EncryptionPair.fromJSON<string, EncString>({
encrypted: "encrypted",
decrypted: "3.data",
});
const jsonObj = JSON.parse(JSON.stringify(accountSettings));
const actual = AccountSettings.fromJSON(jsonObj);
expect(actual.pinProtected).toBeInstanceOf(EncryptionPair);
expect(actual.pinProtected.encrypted).toEqual("encrypted");
expect(actual.pinProtected.decrypted.encryptedString).toEqual("3.data");
});
}); });
}); });

View File

@ -11,7 +11,6 @@ import { DeepJsonify } from "../../../types/deep-jsonify";
import { KdfType } from "../../enums"; import { KdfType } from "../../enums";
import { Utils } from "../../misc/utils"; import { Utils } from "../../misc/utils";
import { EncryptedString, EncString } from "./enc-string";
import { SymmetricCryptoKey } from "./symmetric-crypto-key"; import { SymmetricCryptoKey } from "./symmetric-crypto-key";
export class EncryptionPair<TEncrypted, TDecrypted> { export class EncryptionPair<TEncrypted, TDecrypted> {
@ -148,26 +147,15 @@ export class AccountSettings {
passwordGenerationOptions?: PasswordGeneratorOptions; passwordGenerationOptions?: PasswordGeneratorOptions;
usernameGenerationOptions?: UsernameGeneratorOptions; usernameGenerationOptions?: UsernameGeneratorOptions;
generatorOptions?: GeneratorOptions; generatorOptions?: GeneratorOptions;
pinKeyEncryptedUserKey?: EncryptedString;
pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
protectedPin?: string;
vaultTimeout?: number; vaultTimeout?: number;
vaultTimeoutAction?: string = "lock"; vaultTimeoutAction?: string = "lock";
/** @deprecated July 2023, left for migration purposes*/
pinProtected?: EncryptionPair<string, EncString> = new EncryptionPair<string, EncString>();
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings { static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
if (obj == null) { if (obj == null) {
return null; return null;
} }
return Object.assign(new AccountSettings(), obj, { return Object.assign(new AccountSettings(), obj);
pinProtected: EncryptionPair.fromJSON<string, EncString>(
obj?.pinProtected,
EncString.fromJSON,
),
});
} }
} }

View File

@ -1,6 +1,7 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { firstValueFrom, of, tap } from "rxjs"; import { firstValueFrom, of, tap } from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service"; import { FakeAccountService, mockAccountServiceWith } from "../../../spec/fake-account-service";
import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state"; import { FakeActiveUserState, FakeSingleUserState } from "../../../spec/fake-state";
import { FakeStateProvider } from "../../../spec/fake-state-provider"; import { FakeStateProvider } from "../../../spec/fake-state-provider";
@ -8,7 +9,7 @@ import { KdfConfigService } from "../../auth/abstractions/kdf-config.service";
import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service"; import { FakeMasterPasswordService } from "../../auth/services/master-password/fake-master-password.service";
import { CsprngArray } from "../../types/csprng"; import { CsprngArray } from "../../types/csprng";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { UserKey, MasterKey, PinKey } from "../../types/key"; import { UserKey, MasterKey } from "../../types/key";
import { CryptoFunctionService } from "../abstractions/crypto-function.service"; import { CryptoFunctionService } from "../abstractions/crypto-function.service";
import { EncryptService } from "../abstractions/encrypt.service"; import { EncryptService } from "../abstractions/encrypt.service";
import { KeyGenerationService } from "../abstractions/key-generation.service"; import { KeyGenerationService } from "../abstractions/key-generation.service";
@ -32,6 +33,7 @@ import {
describe("cryptoService", () => { describe("cryptoService", () => {
let cryptoService: CryptoService; let cryptoService: CryptoService;
const pinService = mock<PinServiceAbstraction>();
const keyGenerationService = mock<KeyGenerationService>(); const keyGenerationService = mock<KeyGenerationService>();
const cryptoFunctionService = mock<CryptoFunctionService>(); const cryptoFunctionService = mock<CryptoFunctionService>();
const encryptService = mock<EncryptService>(); const encryptService = mock<EncryptService>();
@ -51,6 +53,7 @@ describe("cryptoService", () => {
stateProvider = new FakeStateProvider(accountService); stateProvider = new FakeStateProvider(accountService);
cryptoService = new CryptoService( cryptoService = new CryptoService(
pinService,
masterPasswordService, masterPasswordService,
keyGenerationService, keyGenerationService,
cryptoFunctionService, cryptoFunctionService,
@ -251,60 +254,50 @@ describe("cryptoService", () => {
}); });
describe("Pin Key refresh", () => { describe("Pin Key refresh", () => {
let cryptoSvcMakePinKey: jest.SpyInstance; const mockPinKeyEncryptedUserKey = new EncString(
const protectedPin = "2.AAAw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg="; );
let encPin: EncString; const mockUserKeyEncryptedPin = new EncString(
"2.BBBw2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=",
);
beforeEach(() => { it("sets a pinKeyEncryptedUserKeyPersistent if a userKeyEncryptedPin and pinKeyEncryptedUserKey is set", async () => {
cryptoSvcMakePinKey = jest.spyOn(cryptoService, "makePinKey"); pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
cryptoSvcMakePinKey.mockResolvedValue(new SymmetricCryptoKey(new Uint8Array(64)) as PinKey); pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
encPin = new EncString( pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(
"2.jcow2vTUePO+CCyokcIfVw==|DTBNlJ5yVsV2Bsk3UU3H6Q==|YvFBff5gxWqM+UsFB6BKimKxhC32AtjF3IStpU1Ijwg=", mockPinKeyEncryptedUserKey,
);
encryptService.encrypt.mockResolvedValue(encPin);
});
it("sets a UserKeyPin if a ProtectedPin and UserKeyPin is set", async () => {
stateService.getProtectedPin.mockResolvedValue(protectedPin);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(
new EncString(
"2.OdGNE3L23GaDZGvu9h2Brw==|/OAcNnrYwu0rjiv8+RUr3Tc+Ef8fV035Tm1rbTxfEuC+2LZtiCAoIvHIZCrM/V1PWnb/pHO2gh9+Koks04YhX8K29ED4FzjeYP8+YQD/dWo=|+12xTcIK/UVRsOyawYudPMHb6+lCHeR2Peq1pQhPm0A=",
),
); );
await cryptoService.setUserKey(mockUserKey, mockUserId); await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(expect.any(EncString), { expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
userId: mockUserId, mockPinKeyEncryptedUserKey,
}); false,
}); mockUserId,
it("sets a PinKeyEphemeral if a ProtectedPin is set, but a UserKeyPin is not set", async () => {
stateService.getProtectedPin.mockResolvedValue(protectedPin);
stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(
expect.any(EncString),
{
userId: mockUserId,
},
); );
}); });
it("clears the UserKeyPin and UserKeyPinEphemeral if the ProtectedPin is not set", async () => { it("sets a pinKeyEncryptedUserKeyEphemeral if a userKeyEncryptedPin is set, but a pinKeyEncryptedUserKey is not set", async () => {
stateService.getProtectedPin.mockResolvedValue(null); pinService.createPinKeyEncryptedUserKey.mockResolvedValue(mockPinKeyEncryptedUserKey);
pinService.getUserKeyEncryptedPin.mockResolvedValue(mockUserKeyEncryptedPin);
pinService.getPinKeyEncryptedUserKeyPersistent.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId); await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(stateService.setPinKeyEncryptedUserKey).toHaveBeenCalledWith(null, { expect(pinService.storePinKeyEncryptedUserKey).toHaveBeenCalledWith(
userId: mockUserId, mockPinKeyEncryptedUserKey,
}); true,
expect(stateService.setPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(null, { mockUserId,
userId: mockUserId, );
}); });
it("clears the pinKeyEncryptedUserKeyPersistent and pinKeyEncryptedUserKeyEphemeral if the UserKeyEncryptedPin is not set", async () => {
pinService.getUserKeyEncryptedPin.mockResolvedValue(null);
await cryptoService.setUserKey(mockUserKey, mockUserId);
expect(pinService.clearPinKeyEncryptedUserKeyPersistent).toHaveBeenCalledWith(mockUserId);
expect(pinService.clearPinKeyEncryptedUserKeyEphemeral).toHaveBeenCalledWith(mockUserId);
}); });
}); });
}); });

View File

@ -1,6 +1,7 @@
import * as bigInt from "big-integer"; import * as bigInt from "big-integer";
import { Observable, filter, firstValueFrom, map } from "rxjs"; import { Observable, filter, firstValueFrom, map } from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data"; import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data";
import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response";
import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response"; import { ProfileProviderOrganizationResponse } from "../../admin-console/models/response/profile-provider-organization.response";
@ -17,7 +18,6 @@ import {
UserKey, UserKey,
MasterKey, MasterKey,
ProviderKey, ProviderKey,
PinKey,
CipherKey, CipherKey,
UserPrivateKey, UserPrivateKey,
UserPublicKey, UserPublicKey,
@ -74,6 +74,7 @@ export class CryptoService implements CryptoServiceAbstraction {
readonly everHadUserKey$: Observable<boolean>; readonly everHadUserKey$: Observable<boolean>;
constructor( constructor(
protected pinService: PinServiceAbstraction,
protected masterPasswordService: InternalMasterPasswordServiceAbstraction, protected masterPasswordService: InternalMasterPasswordServiceAbstraction,
protected keyGenerationService: KeyGenerationService, protected keyGenerationService: KeyGenerationService,
protected cryptoFunctionService: CryptoFunctionService, protected cryptoFunctionService: CryptoFunctionService,
@ -254,7 +255,7 @@ export class CryptoService implements CryptoServiceAbstraction {
if (keySuffix === KeySuffixOptions.Pin) { if (keySuffix === KeySuffixOptions.Pin) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId }); this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId); this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
@ -303,46 +304,6 @@ export class CryptoService implements CryptoServiceAbstraction {
return await this.buildProtectedSymmetricKey(masterKey, userKey.key); return await this.buildProtectedSymmetricKey(masterKey, userKey.key);
} }
// TODO: move to master password service
async decryptUserKeyWithMasterKey(
masterKey: MasterKey,
userKey?: EncString,
userId?: UserId,
): Promise<UserKey> {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
userKey ??= await this.masterPasswordService.getMasterKeyEncryptedUserKey(userId);
masterKey ??= await firstValueFrom(this.masterPasswordService.masterKey$(userId));
if (masterKey == null) {
throw new Error("No master key found.");
}
// Try one more way to get the user key if it still wasn't found.
if (userKey == null) {
const deprecatedKey = await this.stateService.getEncryptedCryptoSymmetricKey({
userId: userId,
});
if (deprecatedKey == null) {
throw new Error("No encrypted user key found.");
}
userKey = new EncString(deprecatedKey);
}
let decUserKey: Uint8Array;
if (userKey.encryptionType === EncryptionType.AesCbc256_B64) {
decUserKey = await this.encryptService.decryptToBytes(userKey, masterKey);
} else if (userKey.encryptionType === EncryptionType.AesCbc256_HmacSha256_B64) {
const newKey = await this.stretchKey(masterKey);
decUserKey = await this.encryptService.decryptToBytes(userKey, newKey);
} else {
throw new Error("Unsupported encryption type.");
}
if (decUserKey == null) {
return null;
}
return new SymmetricCryptoKey(decUserKey) as UserKey;
}
// TODO: move to MasterPasswordService // TODO: move to MasterPasswordService
async hashMasterKey( async hashMasterKey(
password: string, password: string,
@ -548,53 +509,19 @@ export class CryptoService implements CryptoServiceAbstraction {
await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId); await this.stateProvider.setUserState(USER_ENCRYPTED_PRIVATE_KEY, null, userId);
} }
async makePinKey(pin: string, salt: string, kdfConfig: KdfConfig): Promise<PinKey> {
const pinKey = await this.keyGenerationService.deriveKeyFromPassword(pin, salt, kdfConfig);
return (await this.stretchKey(pinKey)) as PinKey;
}
async clearPinKeys(userId?: UserId): Promise<void> { async clearPinKeys(userId?: UserId): Promise<void> {
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId }); userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId });
await this.stateService.setProtectedPin(null, { userId: userId }); if (userId == null) {
throw new Error("Cannot clear PIN keys, no user Id resolved.");
}
await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
await this.pinService.clearUserKeyEncryptedPin(userId);
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId); await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
} }
async decryptUserKeyWithPin(
pin: string,
salt: string,
kdfConfig: KdfConfig,
pinProtectedUserKey?: EncString,
): Promise<UserKey> {
pinProtectedUserKey ||= await this.stateService.getPinKeyEncryptedUserKey();
pinProtectedUserKey ||= await this.stateService.getPinKeyEncryptedUserKeyEphemeral();
if (!pinProtectedUserKey) {
throw new Error("No PIN protected key found.");
}
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
const userKey = await this.encryptService.decryptToBytes(pinProtectedUserKey, pinKey);
return new SymmetricCryptoKey(userKey) as UserKey;
}
// only for migration purposes
async decryptMasterKeyWithPin(
pin: string,
salt: string,
kdfConfig: KdfConfig,
pinProtectedMasterKey?: EncString,
): Promise<MasterKey> {
if (!pinProtectedMasterKey) {
const pinProtectedMasterKeyString = await this.stateService.getEncryptedPinProtected();
if (pinProtectedMasterKeyString == null) {
throw new Error("No PIN protected key found.");
}
pinProtectedMasterKey = new EncString(pinProtectedMasterKeyString);
}
const pinKey = await this.makePinKey(pin, salt, kdfConfig);
const masterKey = await this.encryptService.decryptToBytes(pinProtectedMasterKey, pinKey);
return new SymmetricCryptoKey(masterKey) as MasterKey;
}
async makeSendKey(keyMaterial: CsprngArray): Promise<SymmetricCryptoKey> { async makeSendKey(keyMaterial: CsprngArray): Promise<SymmetricCryptoKey> {
return await this.keyGenerationService.deriveKeyFromMaterial( return await this.keyGenerationService.deriveKeyFromMaterial(
keyMaterial, keyMaterial,
@ -798,6 +725,12 @@ export class CryptoService implements CryptoServiceAbstraction {
* @param userId The desired user * @param userId The desired user
*/ */
protected async storeAdditionalKeys(key: UserKey, userId?: UserId) { protected async storeAdditionalKeys(key: UserKey, userId?: UserId) {
userId ??= await firstValueFrom(this.stateProvider.activeUserId$);
if (userId == null) {
throw new Error("Cannot store additional keys, no user Id resolved.");
}
const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId); const storeAuto = await this.shouldStoreKey(KeySuffixOptions.Auto, userId);
if (storeAuto) { if (storeAuto) {
await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId }); await this.stateService.setUserKeyAutoUnlock(key.keyB64, { userId: userId });
@ -808,37 +741,31 @@ export class CryptoService implements CryptoServiceAbstraction {
const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId); const storePin = await this.shouldStoreKey(KeySuffixOptions.Pin, userId);
if (storePin) { if (storePin) {
await this.storePinKey(key, userId); // Decrypt userKeyEncryptedPin with user key
const pin = await this.encryptService.decryptToUtf8(
await this.pinService.getUserKeyEncryptedPin(userId),
key,
);
const pinKeyEncryptedUserKey = await this.pinService.createPinKeyEncryptedUserKey(
pin,
key,
userId,
);
const noPreExistingPersistentKey =
(await this.pinService.getPinKeyEncryptedUserKeyPersistent(userId)) == null;
await this.pinService.storePinKeyEncryptedUserKey(
pinKeyEncryptedUserKey,
noPreExistingPersistentKey,
userId,
);
// We can't always clear deprecated keys because the pin is only // We can't always clear deprecated keys because the pin is only
// migrated once used to unlock // migrated once used to unlock
await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId); await this.clearDeprecatedKeys(KeySuffixOptions.Pin, userId);
} else { } else {
await this.stateService.setPinKeyEncryptedUserKey(null, { userId: userId }); await this.pinService.clearPinKeyEncryptedUserKeyPersistent(userId);
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId }); await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
}
}
/**
* Stores the pin key if needed. If MP on Reset is enabled, stores the
* ephemeral version.
* @param key The user key
*/
protected async storePinKey(key: UserKey, userId?: UserId) {
const pin = await this.encryptService.decryptToUtf8(
new EncString(await this.stateService.getProtectedPin({ userId: userId })),
key,
);
const pinKey = await this.makePinKey(
pin,
await this.stateService.getEmail({ userId: userId }),
await this.kdfConfigService.getKdfConfig(),
);
const encPin = await this.encryptService.encrypt(key.key, pinKey);
if ((await this.stateService.getPinKeyEncryptedUserKey({ userId: userId })) != null) {
await this.stateService.setPinKeyEncryptedUserKey(encPin, { userId: userId });
} else {
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(encPin, { userId: userId });
} }
} }
@ -851,8 +778,8 @@ export class CryptoService implements CryptoServiceAbstraction {
break; break;
} }
case KeySuffixOptions.Pin: { case KeySuffixOptions.Pin: {
const protectedPin = await this.stateService.getProtectedPin({ userId: userId }); const userKeyEncryptedPin = await this.pinService.getUserKeyEncryptedPin(userId);
shouldStoreKey = !!protectedPin; shouldStoreKey = !!userKeyEncryptedPin;
break; break;
} }
} }
@ -874,16 +801,7 @@ export class CryptoService implements CryptoServiceAbstraction {
protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> { protected async clearAllStoredUserKeys(userId?: UserId): Promise<void> {
await this.stateService.setUserKeyAutoUnlock(null, { userId: userId }); await this.stateService.setUserKeyAutoUnlock(null, { userId: userId });
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(null, { userId: userId }); await this.pinService.clearPinKeyEncryptedUserKeyEphemeral(userId);
}
private async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
const newKey = new Uint8Array(64);
const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256");
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
newKey.set(new Uint8Array(encKey));
newKey.set(new Uint8Array(macKey), 32);
return new SymmetricCryptoKey(newKey);
} }
private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) { private async hashPhrase(hash: Uint8Array, minimumEntropy = 64) {
@ -912,7 +830,7 @@ export class CryptoService implements CryptoServiceAbstraction {
): Promise<[T, EncString]> { ): Promise<[T, EncString]> {
let protectedSymKey: EncString = null; let protectedSymKey: EncString = null;
if (encryptionKey.key.byteLength === 32) { if (encryptionKey.key.byteLength === 32) {
const stretchedEncryptionKey = await this.stretchKey(encryptionKey); const stretchedEncryptionKey = await this.keyGenerationService.stretchKey(encryptionKey);
protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey); protectedSymKey = await this.encryptService.encrypt(newSymKey, stretchedEncryptionKey);
} else if (encryptionKey.key.byteLength === 64) { } else if (encryptionKey.key.byteLength === 64) {
protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey); protectedSymKey = await this.encryptService.encrypt(newSymKey, encryptionKey);
@ -931,42 +849,10 @@ export class CryptoService implements CryptoServiceAbstraction {
if (keySuffix === KeySuffixOptions.Auto) { if (keySuffix === KeySuffixOptions.Auto) {
await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId }); await this.stateService.setCryptoMasterKeyAuto(null, { userId: userId });
} else if (keySuffix === KeySuffixOptions.Pin) { } else if (keySuffix === KeySuffixOptions.Pin) {
await this.stateService.setEncryptedPinProtected(null, { userId: userId }); await this.pinService.clearOldPinKeyEncryptedMasterKey(userId);
await this.stateService.setDecryptedPinProtected(null, { userId: userId });
} }
} }
async decryptAndMigrateOldPinKey(
masterPasswordOnRestart: boolean,
pin: string,
email: string,
kdfConfig: KdfConfig,
oldPinKey: EncString,
): Promise<UserKey> {
// Decrypt
const masterKey = await this.decryptMasterKeyWithPin(pin, email, kdfConfig, oldPinKey);
const encUserKey = await this.stateService.getEncryptedCryptoSymmetricKey();
const userKey = await this.decryptUserKeyWithMasterKey(masterKey, new EncString(encUserKey));
// Migrate
const pinKey = await this.makePinKey(pin, email, kdfConfig);
const pinProtectedKey = await this.encryptService.encrypt(userKey.key, pinKey);
if (masterPasswordOnRestart) {
await this.stateService.setDecryptedPinProtected(null);
await this.stateService.setPinKeyEncryptedUserKeyEphemeral(pinProtectedKey);
} else {
await this.stateService.setEncryptedPinProtected(null);
await this.stateService.setPinKeyEncryptedUserKey(pinProtectedKey);
// We previously only set the protected pin if MP on Restart was enabled
// now we set it regardless
const encPin = await this.encryptService.encrypt(pin, userKey);
await this.stateService.setProtectedPin(encPin.encryptedString);
}
// This also clears the old Biometrics key since the new Biometrics key will
// be created when the user key is set.
await this.stateService.setCryptoMasterKeyBiometric(null);
return userKey;
}
// --DEPRECATED METHODS-- // --DEPRECATED METHODS--
/** /**

View File

@ -81,4 +81,15 @@ export class KeyGenerationService implements KeyGenerationServiceAbstraction {
} }
return new SymmetricCryptoKey(key); return new SymmetricCryptoKey(key);
} }
async stretchKey(key: SymmetricCryptoKey): Promise<SymmetricCryptoKey> {
const newKey = new Uint8Array(64);
const encKey = await this.cryptoFunctionService.hkdfExpand(key.key, "enc", 32, "sha256");
const macKey = await this.cryptoFunctionService.hkdfExpand(key.key, "mac", 32, "sha256");
newKey.set(new Uint8Array(encKey));
newKey.set(new Uint8Array(macKey), 32);
return new SymmetricCryptoKey(newKey);
}
} }

View File

@ -19,7 +19,6 @@ import { HtmlStorageLocation, StorageLocation } from "../enums";
import { StateFactory } from "../factories/state-factory"; import { StateFactory } from "../factories/state-factory";
import { Utils } from "../misc/utils"; import { Utils } from "../misc/utils";
import { Account, AccountData, AccountSettings } from "../models/domain/account"; import { Account, AccountData, AccountSettings } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string";
import { GlobalState } from "../models/domain/global-state"; import { GlobalState } from "../models/domain/global-state";
import { State } from "../models/domain/state"; import { State } from "../models/domain/state";
import { StorageOptions } from "../models/domain/storage-options"; import { StorageOptions } from "../models/domain/storage-options";
@ -220,45 +219,6 @@ export class StateService<
await this.saveSecureStorageKey(partialKeys.userBiometricKey, value, options); await this.saveSecureStorageKey(partialKeys.userBiometricKey, value, options);
} }
async getPinKeyEncryptedUserKey(options?: StorageOptions): Promise<EncString> {
return EncString.fromJSON(
(await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
?.settings?.pinKeyEncryptedUserKey,
);
}
async setPinKeyEncryptedUserKey(value: EncString, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.pinKeyEncryptedUserKey = value?.encryptedString;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getPinKeyEncryptedUserKeyEphemeral(options?: StorageOptions): Promise<EncString> {
return EncString.fromJSON(
(await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())))
?.settings?.pinKeyEncryptedUserKeyEphemeral,
);
}
async setPinKeyEncryptedUserKeyEphemeral(
value: EncString,
options?: StorageOptions,
): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.settings.pinKeyEncryptedUserKeyEphemeral = value?.encryptedString;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
/** /**
* @deprecated Use UserKeyAuto instead * @deprecated Use UserKeyAuto instead
*/ */
@ -369,29 +329,6 @@ export class StateService<
); );
} }
/**
* @deprecated Use getPinKeyEncryptedUserKeyEphemeral instead
*/
async getDecryptedPinProtected(options?: StorageOptions): Promise<EncString> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions()))
)?.settings?.pinProtected?.decrypted;
}
/**
* @deprecated Use setPinKeyEncryptedUserKeyEphemeral instead
*/
async setDecryptedPinProtected(value: EncString, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
account.settings.pinProtected.decrypted = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultInMemoryOptions()),
);
}
async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> { async getDuckDuckGoSharedKey(options?: StorageOptions): Promise<string> {
options = this.reconcileOptions(options, await this.defaultSecureStorageOptions()); options = this.reconcileOptions(options, await this.defaultSecureStorageOptions());
if (options?.userId == null) { if (options?.userId == null) {
@ -512,23 +449,6 @@ export class StateService<
); );
} }
async getEncryptedPinProtected(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.settings?.pinProtected?.encrypted;
}
async setEncryptedPinProtected(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.pinProtected.encrypted = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getIsAuthenticated(options?: StorageOptions): Promise<boolean> { async getIsAuthenticated(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.tokenService.getAccessToken(options?.userId as UserId)) != null && (await this.tokenService.getAccessToken(options?.userId as UserId)) != null &&
@ -645,23 +565,6 @@ export class StateService<
); );
} }
async getProtectedPin(options?: StorageOptions): Promise<string> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.settings?.protectedPin;
}
async setProtectedPin(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.protectedPin = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getUserId(options?: StorageOptions): Promise<string> { async getUserId(options?: StorageOptions): Promise<string> {
return ( return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))

View File

@ -1,5 +1,6 @@
import { firstValueFrom, map, timeout } from "rxjs"; import { firstValueFrom, map, timeout } from "rxjs";
import { PinServiceAbstraction } from "../../../../auth/src/common/abstractions";
import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { VaultTimeoutSettingsService } from "../../abstractions/vault-timeout/vault-timeout-settings.service";
import { AccountService } from "../../auth/abstractions/account.service"; import { AccountService } from "../../auth/abstractions/account.service";
import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthService } from "../../auth/abstractions/auth.service";
@ -20,6 +21,7 @@ export class SystemService implements SystemServiceAbstraction {
private clearClipboardTimeoutFunction: () => Promise<any> = null; private clearClipboardTimeoutFunction: () => Promise<any> = null;
constructor( constructor(
private pinService: PinServiceAbstraction,
private messagingService: MessagingService, private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private reloadCallback: () => Promise<void> = null, private reloadCallback: () => Promise<void> = null,
@ -50,10 +52,13 @@ export class SystemService implements SystemServiceAbstraction {
return; return;
} }
// User has set a PIN, with ask for master password on restart, to protect their vault // If there is an active user, check if they have a pinKeyEncryptedUserKeyEphemeral. If so, prevent process reload upon lock.
const ephemeralPin = await this.stateService.getPinKeyEncryptedUserKeyEphemeral(); const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
if (ephemeralPin != null) { if (userId != null) {
return; const ephemeralPin = await this.pinService.getPinKeyEncryptedUserKeyEphemeral(userId);
if (ephemeralPin != null) {
return;
}
} }
this.cancelProcessReload(); this.cancelProcessReload();

View File

@ -35,31 +35,33 @@ export const BILLING_DISK = new StateDefinition("billing", "disk");
// Auth // Auth
export const ACCOUNT_DISK = new StateDefinition("account", "disk");
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", {
web: "disk-local",
});
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
export const DEVICE_TRUST_DISK_LOCAL = new StateDefinition("deviceTrust", "disk", {
web: "disk-local",
});
export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk"); export const KDF_CONFIG_DISK = new StateDefinition("kdfConfig", "disk");
export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk"); export const KEY_CONNECTOR_DISK = new StateDefinition("keyConnector", "disk");
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
export const ACCOUNT_DISK = new StateDefinition("account", "disk");
export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");
export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
export const AVATAR_DISK = new StateDefinition("avatar", "disk", { web: "disk-local" });
export const ROUTER_DISK = new StateDefinition("router", "disk");
export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", { export const LOGIN_EMAIL_DISK = new StateDefinition("loginEmail", "disk", {
web: "disk-local", web: "disk-local",
}); });
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
export const AUTH_REQUEST_DISK_LOCAL = new StateDefinition("authRequestLocal", "disk", { export const MASTER_PASSWORD_DISK = new StateDefinition("masterPassword", "disk");
web: "disk-local", export const MASTER_PASSWORD_MEMORY = new StateDefinition("masterPassword", "memory");
}); export const PIN_DISK = new StateDefinition("pinUnlock", "disk");
export const PIN_MEMORY = new StateDefinition("pinUnlock", "memory");
export const ROUTER_DISK = new StateDefinition("router", "disk");
export const SSO_DISK = new StateDefinition("ssoLogin", "disk"); export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
export const TOKEN_DISK = new StateDefinition("token", "disk"); export const TOKEN_DISK = new StateDefinition("token", "disk");
export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", {
web: "disk-local", web: "disk-local",
}); });
export const TOKEN_MEMORY = new StateDefinition("token", "memory"); export const TOKEN_MEMORY = new StateDefinition("token", "memory");
export const DEVICE_TRUST_DISK_LOCAL = new StateDefinition("deviceTrust", "disk", { export const TWO_FACTOR_MEMORY = new StateDefinition("twoFactor", "memory");
web: "disk-local",
});
export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk"); export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk");
// Autofill // Autofill

Some files were not shown because too many files have changed in this diff Show More